agentwallet-mcp 1.14.3 → 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 +4 -4
- package/build/index.js +75 -38
- 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"`.
|
|
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).
|
|
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
|
+
- **`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.
|
|
@@ -384,7 +384,7 @@ A per-transaction cap bounds one signature. It cannot see that the five hundredt
|
|
|
384
384
|
|
|
385
385
|
An unknown key stops the server, so a misspelled `budgets` cannot run unbounded. `spend_policy` reports the file, its sha256 (compare it with the file you reviewed), what each budget has spent and has left, and which tools the policy hid. `quote_x402` answers "would this be paid?" for a URL before anything is signed.
|
|
386
386
|
|
|
387
|
-
**The ledger.** Budgets are enforced from an append-only, hash-chained JSONL file (`AGENTWALLET_LEDGER`, or the policy's `ledger`, or `agentwallet-ledger.jsonl` beside the policy when a budget needs one). A spend is reserved on it before the signature exists; a reservation whose signing fails is voided by a later line; the endpoint's or chain's answer is settled onto it afterwards. A spend that may already be on the wire (a broadcast that failed after the bytes left, a hosted request the API host took and then dropped) is kept counted as `unknown` rather than voided; a failure before anything left this process, including a policy or local-mode refusal, is always voided and never consumes budget. The chain is verified on every read, and a ledger that does not verify refuses every spend until the operator has looked at it. Several agentwallet-mcp processes may share one ledger (two MCP clients on one wallet): the file is re-read before every check, and the check-and-append step holds an exclusive lock file (`<ledger>.lock`) across processes, so the budget is one budget however many processes enforce it; a lock older than 30 seconds is treated as left by a dead process. A transaction signed without being broadcast (`sign_transaction`) is reserved and counted like a send, since whoever holds it can broadcast it. `list_payments` reads it, `export_receipts` turns it into CSV or JSONL, and `AGENTWALLET_WEBHOOK_URL` receives every line as it is written.
|
|
387
|
+
**The ledger.** Budgets are enforced from an append-only, hash-chained JSONL file (`AGENTWALLET_LEDGER`, or the policy's `ledger`, or `agentwallet-ledger.jsonl` beside the policy when a budget needs one). A spend is reserved on it before the signature exists; a reservation whose signing fails is voided by a later line; the endpoint's or chain's answer is settled onto it afterwards. A spend that may already be on the wire (a broadcast that failed after the bytes left, a hosted request the API host took and then dropped) is kept counted as `unknown` rather than voided; a failure before anything left this process, including a policy or local-mode refusal, is always voided and never consumes budget. The chain is verified on every read, and a ledger that does not verify refuses every spend until the operator has looked at it. Several agentwallet-mcp processes may share one ledger (two MCP clients on one wallet): the file is re-read before every check, and the check-and-append step holds an exclusive lock file (`<ledger>.lock`) across processes, so the budget is one budget however many processes enforce it; a lock older than 30 seconds is treated as left by a dead process. A transaction signed without being broadcast (`sign_transaction`) is reserved and counted like a send, since whoever holds it can broadcast it. A call that moves two things (a token transfer that also carries native value) is two lines, reserved in turn and released together when either is refused. `list_payments` reads it, `export_receipts` turns it into CSV or JSONL, and `AGENTWALLET_WEBHOOK_URL` receives every line as it is written.
|
|
388
388
|
|
|
389
389
|
### Encrypted keyfiles
|
|
390
390
|
|
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 || '');
|
|
@@ -155,8 +161,12 @@ async function describeSend(body, chainId) {
|
|
|
155
161
|
const token = layout.tokenIndex === undefined ? to : '0x' + wordAt(layout.tokenIndex).slice(24);
|
|
156
162
|
const counterparty = '0x' + wordAt(layout.toIndex).slice(24);
|
|
157
163
|
const amount = BigInt('0x' + wordAt(layout.amountIndex));
|
|
164
|
+
// The call's own value does not vanish because the calldata was
|
|
165
|
+
// recognised: the contract keeps whatever rides along. It is a second
|
|
166
|
+
// spend, native, to the contract (reported privately 2026-10-23, 1.14.4).
|
|
167
|
+
const carried = BigInt(value) > 0n ? { raw: value, to } : undefined;
|
|
158
168
|
if (layout.kind === 'approval' && amount >= UNBOUNDED_APPROVAL) {
|
|
159
|
-
return { kind: 'approval', to: counterparty, asset: token, raw: amount.toString(), decimals: 0, unbounded: true };
|
|
169
|
+
return { kind: 'approval', to: counterparty, asset: token, raw: amount.toString(), decimals: 0, unbounded: true, ...(carried ? { carried } : {}) };
|
|
160
170
|
}
|
|
161
171
|
// Decimals matter here only to price a registry stablecoin (resolved
|
|
162
172
|
// offline) and to print the ledger amount. A token the registry and the
|
|
@@ -171,7 +181,7 @@ async function describeSend(body, chainId) {
|
|
|
171
181
|
catch {
|
|
172
182
|
decimals = 0;
|
|
173
183
|
}
|
|
174
|
-
return { kind: layout.kind, to: counterparty, asset: token, raw: amount.toString(), decimals };
|
|
184
|
+
return { kind: layout.kind, to: counterparty, asset: token, raw: amount.toString(), decimals, ...(carried ? { carried } : {}) };
|
|
175
185
|
}
|
|
176
186
|
}
|
|
177
187
|
if (data && data !== '0x')
|
|
@@ -197,28 +207,40 @@ function assertSendTarget(d, chainId) {
|
|
|
197
207
|
if (!t.ok)
|
|
198
208
|
throw new Error(t.reason + ' Nothing was signed.');
|
|
199
209
|
}
|
|
210
|
+
if (d.carried) {
|
|
211
|
+
const t = targetAllowed(POL(), d.carried.to); // the contract that keeps the value
|
|
212
|
+
if (!t.ok)
|
|
213
|
+
throw new Error(t.reason + ' Nothing was signed.');
|
|
214
|
+
}
|
|
200
215
|
}
|
|
201
216
|
/**
|
|
202
|
-
* The
|
|
203
|
-
* leaves the wallet: a zero-value contract call
|
|
204
|
-
* approval already granted, and approvals are counted
|
|
205
|
-
* An approval's recipient is its spender, so
|
|
206
|
-
* who may be allowed to pull; the native
|
|
207
|
-
*
|
|
217
|
+
* The spends a decoded send makes against the policy, in the order they are
|
|
218
|
+
* reserved; empty when nothing leaves the wallet: a zero-value contract call
|
|
219
|
+
* can only pull what an approval already granted, and approvals are counted
|
|
220
|
+
* when they are granted. An approval's recipient is its spender, so
|
|
221
|
+
* recipients.allow and .deny bind who may be allowed to pull; the native
|
|
222
|
+
* value of a contract call is a spend to that contract; the native value
|
|
223
|
+
* carried on a token or approval call is a second spend, native, to the
|
|
224
|
+
* called contract (1.14.4). Throws for an unbounded approval under a policy.
|
|
208
225
|
*/
|
|
209
|
-
function
|
|
210
|
-
if (d.kind === 'contract' && BigInt(d.raw) === 0n)
|
|
211
|
-
return null;
|
|
226
|
+
function spendInputsOf(d, chainId, tool, wallet, ctx = {}) {
|
|
212
227
|
if (d.kind === 'approval' && d.unbounded && POLICY.path) {
|
|
213
228
|
throw new Error(`policy: an unlimited approval of ${assetLabel(chainId, d.asset)} to ${d.to} cannot be counted against any budget, and under a spend policy every approval is a bounded spend. ` +
|
|
214
229
|
`Approve an exact amount instead. Nothing was signed.`);
|
|
215
230
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
231
|
+
const base = { tool, mode: anyLocalMode() ? 'local' : 'hosted', wallet: anyLocalMode() ? 'local' : wallet, chainId, host: ctx.host ?? null, resource: ctx.resource ?? null };
|
|
232
|
+
const out = [];
|
|
233
|
+
if (!(d.kind === 'contract' && BigInt(d.raw) === 0n)) {
|
|
234
|
+
out.push({
|
|
235
|
+
...base, asset: d.asset, assetLabel: assetLabel(chainId, d.asset), raw: d.raw, decimals: d.decimals,
|
|
236
|
+
usd: d.unbounded ? null : usdOf(chainId, d.asset, d.raw, d.decimals),
|
|
237
|
+
to: d.to, method: ctx.method ?? methodOf(d.kind),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
if (d.carried) {
|
|
241
|
+
out.push({ ...base, asset: '', assetLabel: assetLabel(chainId, ''), raw: d.carried.raw, decimals: nativeDecimals(chainId), usd: null, to: d.carried.to, method: 'native' });
|
|
242
|
+
}
|
|
243
|
+
return out;
|
|
222
244
|
}
|
|
223
245
|
// ─── Configuration ──────────────────────────────────────────────
|
|
224
246
|
const API_BASE = (process.env.AGENTWALLET_API_URL || 'https://hifriendbot.com/wp-json/agentwallet/v1').trim().replace(/\/+$/, '');
|
|
@@ -444,7 +466,7 @@ async function api(path, method = 'GET', body, extraHeaders) {
|
|
|
444
466
|
extraHeaders = { ...extraHeaders };
|
|
445
467
|
delete extraHeaders['X-AGW-CTX'];
|
|
446
468
|
}
|
|
447
|
-
|
|
469
|
+
const reserved = [];
|
|
448
470
|
// /sign with a transaction body is the same spend as /send: the caller gets
|
|
449
471
|
// a broadcast-ready raw transaction (2026-10-14 report, 1.14.2). A message
|
|
450
472
|
// signature (body.message) moves nothing and is not gated.
|
|
@@ -454,18 +476,28 @@ async function api(path, method = 'GET', body, extraHeaders) {
|
|
|
454
476
|
const chainId = localChainId(body, new URLSearchParams(path.split('?')[1] || ''));
|
|
455
477
|
const d = await describeSend(body, chainId);
|
|
456
478
|
assertSendTarget(d, chainId);
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
479
|
+
const tool = ctx.tool || (m[2] === 'sign' ? 'sign_transaction' : 'send');
|
|
480
|
+
// One line per spend the call makes (a token call that also carries native
|
|
481
|
+
// value makes two); a refusal of a later one releases the earlier ones.
|
|
482
|
+
for (const input of spendInputsOf(d, chainId, tool, m[1], ctx)) {
|
|
483
|
+
try {
|
|
484
|
+
reserved.push(reserveSpend(input));
|
|
485
|
+
}
|
|
486
|
+
catch (e) {
|
|
487
|
+
for (const seq of reserved)
|
|
488
|
+
LEDGER.void(seq, `released, the same call was refused: ${e.message}`);
|
|
489
|
+
throw e;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
460
492
|
}
|
|
461
493
|
try {
|
|
462
494
|
const out = await apiInner(path, method, body, extraHeaders);
|
|
463
|
-
|
|
495
|
+
for (const seq of reserved) {
|
|
464
496
|
const r = out;
|
|
465
497
|
if (m[2] === 'sign')
|
|
466
|
-
LEDGER.settle(
|
|
498
|
+
LEDGER.settle(seq, 'signed', 'raw transaction returned to the caller; not broadcast by this process, so it stays counted');
|
|
467
499
|
else
|
|
468
|
-
LEDGER.settle(
|
|
500
|
+
LEDGER.settle(seq, 'broadcast', typeof r?.tx_hash === 'string' ? r.tx_hash : typeof r?.signature === 'string' ? r.signature : null);
|
|
469
501
|
}
|
|
470
502
|
return out;
|
|
471
503
|
}
|
|
@@ -476,12 +508,12 @@ async function api(path, method = 'GET', body, extraHeaders) {
|
|
|
476
508
|
// message and the word "broadcasts" in the local-mode refusal of
|
|
477
509
|
// transaction signing made every refused sign_transaction consume the
|
|
478
510
|
// budget for good (reported privately 2026-10-16, 1.14.3).
|
|
479
|
-
|
|
511
|
+
for (const seq of reserved) {
|
|
480
512
|
const msg = e.message;
|
|
481
513
|
if (isMaybeLive(e))
|
|
482
|
-
LEDGER.settle(
|
|
514
|
+
LEDGER.settle(seq, 'unknown', msg);
|
|
483
515
|
else
|
|
484
|
-
LEDGER.void(
|
|
516
|
+
LEDGER.void(seq, msg);
|
|
485
517
|
}
|
|
486
518
|
throw e;
|
|
487
519
|
}
|
|
@@ -757,8 +789,8 @@ async function handleX402Payment(x402Data, originalPath, originalMethod, origina
|
|
|
757
789
|
}
|
|
758
790
|
// Send payment using our wallet (with X-AGW-SKIP-X402 to prevent recursion).
|
|
759
791
|
// The ledger context names the API host and route; the /send hook reserves and settles the spend.
|
|
760
|
-
const ctx = { 'X-AGW-SKIP-X402': 'true', 'X-AGW-CTX': JSON.stringify({ tool: 'autopay', host:
|
|
761
|
-
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));
|
|
762
794
|
if (!hostRule.ok)
|
|
763
795
|
throw new Error(`x402 auto-pay refused: ${hostRule.reason}`);
|
|
764
796
|
let txHash;
|
|
@@ -1143,7 +1175,7 @@ function resolveChainId(network) {
|
|
|
1143
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');
|
|
1144
1176
|
const server = new McpServer({
|
|
1145
1177
|
name: 'agentwallet',
|
|
1146
|
-
version: '1.14.
|
|
1178
|
+
version: '1.14.5',
|
|
1147
1179
|
}, {
|
|
1148
1180
|
instructions: `AgentWallet gives AI agents their own blockchain wallets. ${anyLocalMode()
|
|
1149
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.'
|
|
@@ -1864,9 +1896,9 @@ tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the server
|
|
|
1864
1896
|
}
|
|
1865
1897
|
// A host the policy denies is never contacted at all, not even for the quote.
|
|
1866
1898
|
{
|
|
1867
|
-
const early = hostAllowed(POL(),
|
|
1899
|
+
const early = hostAllowed(POL(), hostOf(url));
|
|
1868
1900
|
if (!early.ok)
|
|
1869
|
-
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.' });
|
|
1870
1902
|
}
|
|
1871
1903
|
// Validate URL: canonicalizes IP literals (including IPv4-mapped IPv6) and
|
|
1872
1904
|
// resolves DNS names, rejecting any private/internal destination. An early
|
|
@@ -2004,7 +2036,7 @@ tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the server
|
|
|
2004
2036
|
// Step 3e: Spend policy. The host, chain, asset and recipient lists, the
|
|
2005
2037
|
// per-host ceiling, and whether the rolling budgets have room. Nothing is
|
|
2006
2038
|
// recorded here; the reservation is written at signing time.
|
|
2007
|
-
const payHost =
|
|
2039
|
+
const payHost = hostOf(url);
|
|
2008
2040
|
const hostLimit = perHostRule(POL(), payHost);
|
|
2009
2041
|
const amount18 = rawToUnits(rawAmount, trustedDecimals);
|
|
2010
2042
|
const policyInput = {
|
|
@@ -2624,7 +2656,7 @@ tool('quote_x402', 'Ask what an x402 endpoint charges without paying: fetches th
|
|
|
2624
2656
|
reqHeaders[k] = v;
|
|
2625
2657
|
}
|
|
2626
2658
|
}
|
|
2627
|
-
const host =
|
|
2659
|
+
const host = hostOf(url);
|
|
2628
2660
|
{
|
|
2629
2661
|
const early = hostAllowed(POL(), host);
|
|
2630
2662
|
if (!early.ok)
|
|
@@ -2815,8 +2847,12 @@ tool('simulate_transaction', 'Dry-run an EVM transaction with nothing signed: th
|
|
|
2815
2847
|
const policy = (() => {
|
|
2816
2848
|
try {
|
|
2817
2849
|
assertSendTarget(d, chain_id);
|
|
2818
|
-
const input
|
|
2819
|
-
|
|
2850
|
+
for (const input of spendInputsOf(d, chain_id, 'simulate_transaction', String(wallet_id))) {
|
|
2851
|
+
const v = spendVerdict(input);
|
|
2852
|
+
if (!v.ok)
|
|
2853
|
+
return v;
|
|
2854
|
+
}
|
|
2855
|
+
return { ok: true };
|
|
2820
2856
|
}
|
|
2821
2857
|
catch (e) {
|
|
2822
2858
|
return { ok: false, reason: e.message };
|
|
@@ -2826,6 +2862,7 @@ tool('simulate_transaction', 'Dry-run an EVM transaction with nothing signed: th
|
|
|
2826
2862
|
kind: d.kind, to: d.to, asset: d.asset || 'native', asset_label: assetLabel(chain_id, d.asset),
|
|
2827
2863
|
amount: d.unbounded ? 'unlimited' : formatUnits(d.raw, d.decimals),
|
|
2828
2864
|
...(d.kind === 'approval' ? { note: 'an approval: `to` is the spender being allowed to pull `amount` of `asset`' } : {}),
|
|
2865
|
+
...(d.carried ? { carried_native: { amount: formatUnits(d.carried.raw, nativeDecimals(chain_id)), to: d.carried.to, note: 'native value sent along with the calldata: a second spend, against native_budget, to the called contract' } } : {}),
|
|
2829
2866
|
};
|
|
2830
2867
|
if (isLocalMode()) {
|
|
2831
2868
|
const sim = await localSimulate(chain_id, to, BigInt(valueRaw), data && data !== '0x' ? data : undefined);
|
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",
|