agentwallet-mcp 1.14.4 → 1.14.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -373,10 +373,10 @@ A per-transaction cap bounds one signature. It cannot see that the five hundredt
373
373
  }
374
374
  ```
375
375
 
376
- - **`budget`**: rolling dollar budgets (last 24 hours, last 30 days, lifetime) across every chain, counting x402 payments, stablecoin transfers and stablecoin approvals in registry stablecoins. A spend the budget cannot price (a token outside the registry) is refused while a budget is set, unless `"unpriced_spends": "allow"`.
376
+ - **`budget`**: rolling dollar budgets (last 24 hours, last 30 days, lifetime) across every chain, counting x402 payments, stablecoin transfers and stablecoin approvals in registry stablecoins. A spend the budget cannot price (a token outside the registry) is refused while a budget is set, unless `"unpriced_spends": "allow"`. An SPL send is priced at the mint's own decimals; a `token_decimals` hint that disagrees with the mint is refused before any request is made.
377
377
  - **`native_budget`**: the same per chain, in the chain's own coin, counting native transfers and the native value sent into any contract (`wrap_eth`, `send_transaction` with value and calldata), including value carried on a token transfer or approval call (a payable token contract keeps it, so it is a second spend, to that contract).
378
378
  - **Approvals are spends.** An ERC-20 allowance (`approve_token`, `approve_permit2`, `increaseAllowance`, `approveAndCall`, Permit2 `approve`, or the same calldata through `send_transaction`) is what the spender will take later, so it is priced, checked and reserved on the ledger when it is granted, the spender is checked against the recipient lists, and an unlimited approval (2^128 base units or more) is refused outright, since no budget can count it; grant an exact amount. A zero-value call to any other contract (a router, a bridge) reserves nothing: it can only pull what an approval already granted.
379
- - **`hosts`**, **`per_host`**: which endpoints `pay_x402` may pay, and how much per host per day, per month and per payment. A denied host is never contacted, not even for a quote.
379
+ - **`hosts`**, **`per_host`**: which endpoints `pay_x402` may pay, and how much per host per day, per month and per payment. A denied host is never contacted, not even for a quote. A host is matched by its canonical DNS name (lowercase, the root label's trailing dot removed), so `api.example.com.` and its percent-encoded spelling are `api.example.com` to every list and to the ledger's per-host sums.
380
380
  - **`recipients`**: where funds may go (transfers, x402 `payTo`, approval spenders, contracts that receive native value). The allow list covers those funds destinations; the contract a zero-value call is aimed at (a token being approved, a router) is checked against the deny list and the chain list.
381
381
  - **`chains`**, **`assets`**: allow lists; an asset entry can be scoped `"8453:0x..."` or be the word `native`.
382
382
  - **`approval`**: above `above`, every x402 payment is put to the human at the MCP client through [elicitation](https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation) (the client shows the amount, token, recipient and resource and asks yes or no; a client that declared no elicitation support cannot answer, so the payment is refused). In local mode an accepted answer may admit a payment above the per-payment cap, up to `max`. Budgets are never lifted by anyone's approval.
package/build/index.js CHANGED
@@ -32,7 +32,7 @@ import { assessTokenRisk } from './token-risk.js';
32
32
  import { isLegacyAgentWalletAccept, requiredAmount, buildAuthorization, buildPaymentPayload, buildPaymentPayloadRaw, paymentHeaderFor, parsePaymentRequired, parseSettlement, pickOption, knownTokenDomain, decodeAbiString as decodeDomainString, parseEip7702Delegation, erc1271ProbeCalldata, classifyErc1271Probe, settlementVerdict, legacyReceiptAllowed, sanitizeRequired, } from './x402-eip3009.js';
33
33
  import { isSolanaLocalMode, getSolanaAddress, solanaWalletRecord, localSolBalance, localSplBalance, localSolTransfer, localSplTransfer, resolveSolanaRpc, mintDecimalsStrict, } from './local-solana.js';
34
34
  import { localSimulate } from './local-wallet.js';
35
- import { loadPolicy, describePolicy, hostAllowed, recipientAllowed, targetAllowed, chainAllowed, assetAllowed, perHostRule, toolAllowed, budgetReport, units as policyUnits, human as policyHuman, rawToUnits, } from './policy.js';
35
+ import { loadPolicy, describePolicy, hostAllowed, recipientAllowed, targetAllowed, chainAllowed, assetAllowed, perHostRule, toolAllowed, hostOf, budgetReport, units as policyUnits, human as policyHuman, rawToUnits, } from './policy.js';
36
36
  import { Ledger } from './ledger.js';
37
37
  import { markMaybeLive, isMaybeLive, carryMaybeLive, connectionNeverMade } from './maybe-live.js';
38
38
  import { discover } from './discovery.js';
@@ -135,7 +135,13 @@ async function describeSend(body, chainId) {
135
135
  if (body.token_mint) {
136
136
  const mint = String(body.token_mint);
137
137
  const d = Number(body.token_decimals);
138
- return { kind: 'spl', to: String(body.to || ''), asset: mint, raw: String(body.value ?? '0'), decimals: Number.isInteger(d) && d >= 0 && d <= 36 ? d : await verifiedDecimals(chainId, mint) };
138
+ // The caller's token_decimals only ever confirms the mint's own value: in
139
+ // hosted mode it was taken as given, so "18" for USDC priced a 1,000 USDC
140
+ // send at a trillionth of a dollar against the budget and the request
141
+ // reached the custodian (reported privately 2026-10-24, 1.14.5). The chain
142
+ // would have refused the TransferChecked, but the ledger was already wrong.
143
+ const given = Number.isInteger(d) && d >= 0 && d <= 36 ? d : undefined;
144
+ return { kind: 'spl', to: String(body.to || ''), asset: mint, raw: String(body.value ?? '0'), decimals: await verifiedDecimals(chainId, mint, given) };
139
145
  }
140
146
  const data = String(body.data || '');
141
147
  const to = String(body.to || '');
@@ -783,8 +789,8 @@ async function handleX402Payment(x402Data, originalPath, originalMethod, origina
783
789
  }
784
790
  // Send payment using our wallet (with X-AGW-SKIP-X402 to prevent recursion).
785
791
  // The ledger context names the API host and route; the /send hook reserves and settles the spend.
786
- const ctx = { 'X-AGW-SKIP-X402': 'true', 'X-AGW-CTX': JSON.stringify({ tool: 'autopay', host: new URL(API_BASE).hostname, resource: `${originalMethod} ${originalPath.split('?')[0]}`, method: 'autopay' }) };
787
- const hostRule = hostAllowed(POL(), new URL(API_BASE).hostname);
792
+ const ctx = { 'X-AGW-SKIP-X402': 'true', 'X-AGW-CTX': JSON.stringify({ tool: 'autopay', host: hostOf(API_BASE), resource: `${originalMethod} ${originalPath.split('?')[0]}`, method: 'autopay' }) };
793
+ const hostRule = hostAllowed(POL(), hostOf(API_BASE));
788
794
  if (!hostRule.ok)
789
795
  throw new Error(`x402 auto-pay refused: ${hostRule.reason}`);
790
796
  let txHash;
@@ -1169,7 +1175,7 @@ function resolveChainId(network) {
1169
1175
  const AddressSchema = z.string().regex(/^(0x[a-fA-F0-9]{40}|[1-9A-HJ-NP-Za-km-z]{32,44})$/, 'Must be a 0x-prefixed 40-hex EVM address or a base58 Solana address');
1170
1176
  const server = new McpServer({
1171
1177
  name: 'agentwallet',
1172
- version: '1.14.4',
1178
+ version: '1.14.6',
1173
1179
  }, {
1174
1180
  instructions: `AgentWallet gives AI agents their own blockchain wallets. ${anyLocalMode()
1175
1181
  ? 'This server runs in LOCAL (self-custody) mode: the operator supplied the key, signing happens in this process, and the only spend guards are the environment caps (AGENTWALLET_MAX_TX_NATIVE / _TOKEN / _SOL per transaction, AGENTWALLET_MAX_AUTOPAY and AGENTWALLET_AUTOPAY_ASSETS for x402, AGENTWALLET_X402_MAX_TIMEOUT for authorization windows). There is no server-side pause, daily limit or approval email; pause_wallet, unpause_wallet and delete_wallet are refused.'
@@ -1890,9 +1896,9 @@ tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the server
1890
1896
  }
1891
1897
  // A host the policy denies is never contacted at all, not even for the quote.
1892
1898
  {
1893
- const early = hostAllowed(POL(), new URL(url).hostname);
1899
+ const early = hostAllowed(POL(), hostOf(url));
1894
1900
  if (!early.ok)
1895
- return jsonResponse({ status: null, payment_required: null, payment_made: false, policy_refusal: true, host: new URL(url).hostname, error: early.reason + ' The endpoint was not contacted.' });
1901
+ return jsonResponse({ status: null, payment_required: null, payment_made: false, policy_refusal: true, host: hostOf(url), error: early.reason + ' The endpoint was not contacted.' });
1896
1902
  }
1897
1903
  // Validate URL: canonicalizes IP literals (including IPv4-mapped IPv6) and
1898
1904
  // resolves DNS names, rejecting any private/internal destination. An early
@@ -2030,7 +2036,7 @@ tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the server
2030
2036
  // Step 3e: Spend policy. The host, chain, asset and recipient lists, the
2031
2037
  // per-host ceiling, and whether the rolling budgets have room. Nothing is
2032
2038
  // recorded here; the reservation is written at signing time.
2033
- const payHost = new URL(url).hostname;
2039
+ const payHost = hostOf(url);
2034
2040
  const hostLimit = perHostRule(POL(), payHost);
2035
2041
  const amount18 = rawToUnits(rawAmount, trustedDecimals);
2036
2042
  const policyInput = {
@@ -2650,7 +2656,7 @@ tool('quote_x402', 'Ask what an x402 endpoint charges without paying: fetches th
2650
2656
  reqHeaders[k] = v;
2651
2657
  }
2652
2658
  }
2653
- const host = new URL(url).hostname;
2659
+ const host = hostOf(url);
2654
2660
  {
2655
2661
  const early = hostAllowed(POL(), host);
2656
2662
  if (!early.ok)
package/build/ledger.js CHANGED
@@ -18,7 +18,7 @@
18
18
  */
19
19
  import { appendFileSync, existsSync, readFileSync, openSync, closeSync, writeSync, unlinkSync, statSync } from 'node:fs';
20
20
  import { createHash, createHmac } from 'node:crypto';
21
- import { DAY_MS, MONTH_MS, units, rawToUnits, human } from './policy.js';
21
+ import { DAY_MS, MONTH_MS, units, rawToUnits, human, canonicalHost } from './policy.js';
22
22
  const GENESIS = '0'.repeat(64);
23
23
  const MAX_LINE = 16 * 1024;
24
24
  /** How long a spend waits for another process to finish its own check-and-append. */
@@ -190,7 +190,10 @@ export class Ledger {
190
190
  return this.sumNative(chainId, nowMs);
191
191
  }
192
192
  sumUsd(nowMs, host) {
193
- return this.sum(nowMs, e => (host === undefined || e.host === host) && e.usd !== null ? units(e.usd) : 0n);
193
+ // Lines are written with the canonical host, but a ledger from before 1.14.5
194
+ // may hold "api.example.com." lines; they belong to the same host's sum.
195
+ const want = host === undefined ? undefined : canonicalHost(host);
196
+ return this.sum(nowMs, e => (want === undefined || (e.host !== null && canonicalHost(e.host) === want)) && e.usd !== null ? units(e.usd) : 0n);
194
197
  }
195
198
  sumNative(chainId, nowMs) {
196
199
  return this.sum(nowMs, e => e.chain_id === chainId && e.asset === '' ? rawToUnits(e.raw, nativeDecimalsOf(chainId)) : 0n);
@@ -303,7 +306,7 @@ export class Ledger {
303
306
  const e = this.entries[i];
304
307
  if (e.kind !== 'spend')
305
308
  continue;
306
- if (opts.host !== undefined && e.host !== opts.host)
309
+ if (opts.host !== undefined && (e.host === null || canonicalHost(e.host) !== canonicalHost(opts.host)))
307
310
  continue;
308
311
  if (opts.since !== undefined && Date.parse(e.ts) < opts.since)
309
312
  continue;
package/build/policy.d.ts CHANGED
@@ -218,6 +218,16 @@ export declare function parsePolicy(text: string): Policy;
218
218
  export declare function loadPolicy(env?: NodeJS.ProcessEnv): LoadedPolicy;
219
219
  /** A budget is only meaningful across restarts, so it needs a file. */
220
220
  export declare function needsLedger(p: Policy): boolean;
221
+ /**
222
+ * A DNS name as the policy compares, sums and records it: lowercase, without
223
+ * the root label's trailing dot. "api.example.com." (and its %2E spelling)
224
+ * names the same host as "api.example.com", TLS accepts either, and Node's URL
225
+ * parser keeps the dot, so the raw hostname slipped past hosts.deny and
226
+ * per_host (reported privately 2026-10-24, 1.14.5). IP literals pass through.
227
+ */
228
+ export declare function canonicalHost(host: string): string;
229
+ /** The host of a URL as the policy sees it. */
230
+ export declare function hostOf(url: string): string;
221
231
  export declare function hostMatches(pattern: string, host: string): boolean;
222
232
  export declare function hostAllowed(p: Policy, host: string): {
223
233
  ok: true;
package/build/policy.js CHANGED
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import { readFileSync } from 'node:fs';
15
15
  import { createHash } from 'node:crypto';
16
+ import { dirname, isAbsolute, join } from 'node:path';
16
17
  import { z } from 'zod';
17
18
  const Dec = z.string().regex(/^\d+(\.\d+)?$/, 'a decimal number as a string, like "5" or "0.25"');
18
19
  const Window = z.object({ daily: Dec.optional(), monthly: Dec.optional(), total: Dec.optional() }).strict();
@@ -118,12 +119,15 @@ export function loadPolicy(env = process.env) {
118
119
  }
119
120
  const policy = parsePolicy(text);
120
121
  const hash = createHash('sha256').update(text).digest('hex');
121
- const dir = file.replace(/[\\/][^\\/]*$/, '') || '.';
122
+ // The ledger lives beside the policy file. dirname() gives '.' for a bare
123
+ // filename; a hand-rolled strip-after-last-slash left the filename itself as
124
+ // the directory, so AGENTWALLET_POLICY=policy.json could not start (1.14.6).
125
+ const dir = dirname(file);
122
126
  let ledgerPath = ledgerEnv || null;
123
127
  if (!ledgerPath && policy.ledger)
124
- ledgerPath = /^([a-zA-Z]:)?[\\/]/.test(policy.ledger) ? policy.ledger : `${dir}/${policy.ledger}`;
128
+ ledgerPath = isAbsolute(policy.ledger) ? policy.ledger : join(dir, policy.ledger);
125
129
  if (!ledgerPath && needsLedger(policy))
126
- ledgerPath = `${dir}/agentwallet-ledger.jsonl`;
130
+ ledgerPath = join(dir, 'agentwallet-ledger.jsonl');
127
131
  return { policy, path: file, hash, ledgerPath };
128
132
  }
129
133
  /** A budget is only meaningful across restarts, so it needs a file. */
@@ -131,9 +135,28 @@ export function needsLedger(p) {
131
135
  return Boolean(p.budget || p.native_budget || (p.per_host && Object.values(p.per_host).some(h => h.daily || h.monthly)));
132
136
  }
133
137
  /* ── Matching ─────────────────────────────────────────────────────── */
138
+ /**
139
+ * A DNS name as the policy compares, sums and records it: lowercase, without
140
+ * the root label's trailing dot. "api.example.com." (and its %2E spelling)
141
+ * names the same host as "api.example.com", TLS accepts either, and Node's URL
142
+ * parser keeps the dot, so the raw hostname slipped past hosts.deny and
143
+ * per_host (reported privately 2026-10-24, 1.14.5). IP literals pass through.
144
+ */
145
+ export function canonicalHost(host) {
146
+ let h = host.trim().toLowerCase();
147
+ if (h.startsWith('['))
148
+ return h;
149
+ while (h.endsWith('.'))
150
+ h = h.slice(0, -1);
151
+ return h;
152
+ }
153
+ /** The host of a URL as the policy sees it. */
154
+ export function hostOf(url) {
155
+ return canonicalHost(new URL(url).hostname);
156
+ }
134
157
  export function hostMatches(pattern, host) {
135
- const p = pattern.toLowerCase();
136
- const h = host.toLowerCase();
158
+ const p = canonicalHost(pattern);
159
+ const h = canonicalHost(host);
137
160
  if (p.startsWith('*.'))
138
161
  return h.endsWith(p.slice(1)) && h.length > p.length - 1;
139
162
  return p === h;
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/hifriendbot/agentwallet-mcp.git"
7
7
  },
8
- "version": "1.14.4",
8
+ "version": "1.14.6",
9
9
  "description": "MCP server for AgentWallet: self-custody local signing on EVM and Solana, or hosted wallets. Sign, broadcast, pay x402 invoices, and accept x402 payments.",
10
10
  "type": "module",
11
11
  "main": "build/index.js",
@@ -20,7 +20,7 @@
20
20
  "scripts": {
21
21
  "build": "tsc",
22
22
  "start": "node build/index.js",
23
- "test": "node test/x402-payment.test.mjs && node test/eip3009.test.mjs && node test/permit2-risk.test.mjs && node test/token-cap.test.mjs && node test/spl-cap.test.mjs && node test/unknown-decimals-cap.test.mjs && node test/decimals-range-cap.test.mjs && node test/local-mode.test.mjs && node test/ssrf-guard.test.mjs && node test/x402-policy.test.mjs && node test/local-guard-shape.test.mjs && node test/allowance-total-cap.test.mjs && node test/autopay-scheme.test.mjs && node test/spl-recipient-guard.test.mjs && node test/approval-binding.test.mjs && node test/ata-rent-size.test.mjs && node test/final-url-guard.test.mjs && node test/audit-1135.test.mjs && node test/local-no-autopay.test.mjs && node test/audit-1136.test.mjs && node test/delete-confirm.test.mjs && node test/audit-1138.test.mjs && node test/audit-1139.test.mjs && node test/audit-11310.test.mjs && node test/audit-11311.test.mjs && node test/audit-11312.test.mjs && node test/audit-11313.test.mjs && node test/audit-11316.test.mjs && node test/spend-policy.test.mjs",
23
+ "test": "node test/x402-payment.test.mjs && node test/eip3009.test.mjs && node test/permit2-risk.test.mjs && node test/token-cap.test.mjs && node test/spl-cap.test.mjs && node test/unknown-decimals-cap.test.mjs && node test/decimals-range-cap.test.mjs && node test/local-mode.test.mjs && node test/ssrf-guard.test.mjs && node test/x402-policy.test.mjs && node test/local-guard-shape.test.mjs && node test/allowance-total-cap.test.mjs && node test/autopay-scheme.test.mjs && node test/spl-recipient-guard.test.mjs && node test/approval-binding.test.mjs && node test/ata-rent-size.test.mjs && node test/final-url-guard.test.mjs && node test/audit-1135.test.mjs && node test/local-no-autopay.test.mjs && node test/audit-1136.test.mjs && node test/delete-confirm.test.mjs && node test/audit-1138.test.mjs && node test/audit-1139.test.mjs && node test/audit-11310.test.mjs && node test/audit-11311.test.mjs && node test/audit-11312.test.mjs && node test/audit-11313.test.mjs && node test/audit-11316.test.mjs && node test/spend-policy.test.mjs && node test/policy-path-resolution.test.mjs",
24
24
  "prepublishOnly": "npm run build"
25
25
  },
26
26
  "keywords": [