agentwallet-mcp 1.14.4 → 1.14.5
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 +2 -2
- package/build/index.js +15 -9
- package/build/ledger.js +6 -3
- package/build/policy.d.ts +10 -0
- package/build/policy.js +21 -2
- package/package.json +1 -1
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
|
-
|
|
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:
|
|
787
|
-
const hostRule = hostAllowed(POL(),
|
|
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.
|
|
1178
|
+
version: '1.14.5',
|
|
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(),
|
|
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:
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
@@ -131,9 +131,28 @@ export function needsLedger(p) {
|
|
|
131
131
|
return Boolean(p.budget || p.native_budget || (p.per_host && Object.values(p.per_host).some(h => h.daily || h.monthly)));
|
|
132
132
|
}
|
|
133
133
|
/* ── Matching ─────────────────────────────────────────────────────── */
|
|
134
|
+
/**
|
|
135
|
+
* A DNS name as the policy compares, sums and records it: lowercase, without
|
|
136
|
+
* the root label's trailing dot. "api.example.com." (and its %2E spelling)
|
|
137
|
+
* names the same host as "api.example.com", TLS accepts either, and Node's URL
|
|
138
|
+
* parser keeps the dot, so the raw hostname slipped past hosts.deny and
|
|
139
|
+
* per_host (reported privately 2026-10-24, 1.14.5). IP literals pass through.
|
|
140
|
+
*/
|
|
141
|
+
export function canonicalHost(host) {
|
|
142
|
+
let h = host.trim().toLowerCase();
|
|
143
|
+
if (h.startsWith('['))
|
|
144
|
+
return h;
|
|
145
|
+
while (h.endsWith('.'))
|
|
146
|
+
h = h.slice(0, -1);
|
|
147
|
+
return h;
|
|
148
|
+
}
|
|
149
|
+
/** The host of a URL as the policy sees it. */
|
|
150
|
+
export function hostOf(url) {
|
|
151
|
+
return canonicalHost(new URL(url).hostname);
|
|
152
|
+
}
|
|
134
153
|
export function hostMatches(pattern, host) {
|
|
135
|
-
const p = pattern
|
|
136
|
-
const h = host
|
|
154
|
+
const p = canonicalHost(pattern);
|
|
155
|
+
const h = canonicalHost(host);
|
|
137
156
|
if (p.startsWith('*.'))
|
|
138
157
|
return h.endsWith(p.slice(1)) && h.length > p.length - 1;
|
|
139
158
|
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.
|
|
8
|
+
"version": "1.14.5",
|
|
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",
|