agentwallet-mcp 1.14.8 → 1.14.9
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 +33 -1
- package/build/local-wallet.js +8 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ Use `AGENTWALLET_KEYFILE=/path/to/key` instead if you would rather keep the key
|
|
|
43
43
|
|
|
44
44
|
`AGENTWALLET_MAX_TX_NATIVE` is a per-transaction ceiling in native units (ETH, MATIC and so on). In hosted mode the server enforces limits; in local mode there is no server, so these guards are the only ones there are. Set them.
|
|
45
45
|
|
|
46
|
-
`AGENTWALLET_MAX_TX_TOKEN` is the equivalent ceiling for ERC-20 movement, in human units of the token. **Set this one too if you hold stablecoins.** The native cap cannot see a token transfer: an ERC-20 send carries `value = 0` with the amount in the calldata, so `AGENTWALLET_MAX_TX_NATIVE` alone leaves a USDC balance uncapped. `AGENTWALLET_MAX_TX_TOKEN` covers `transfer`, `transferFrom`, `approve`, `increaseAllowance`, Permit2 `approve`, and `deposit()` / `withdraw(uint256)` on the chain's own wrapped-native contract (so `wrap_eth` and `unwrap_eth` keep working under the cap), the approvals because an unbounded allowance is a drain waiting to happen. ERC-721 and ERC-1155 calldata (`setApprovalForAll`, `safeTransferFrom`) is refused while the cap is set, since nothing prices an NFT; without the cap it passes the way an ERC-20 transfer of any size does, except that an operator grant is an unlimited approval and is refused everywhere by default (see `AGENTWALLET_ALLOW_UNLIMITED_APPROVAL`). Any other calldata sent to a token contract or to Permit2 is refused while the cap is set, because the guard cannot price it; set `AGENTWALLET_ALLOW_UNKNOWN_TOKEN_CALLS=1` to allow such calls deliberately. `increaseAllowance` is judged on the resulting total: the current allowance is read first and the call refuses if it cannot be, so two compliant increments cannot stack a standing allowance above the cap. Calls to other contracts (routers, bridges) pass, since they can only pull what an approval already allowed; a contract counts as "other" only when it demonstrably declines `decimals()`, so an absurd or malformed answer, or an RPC that cannot be reached, refuses rather than allows. Decimals are resolved locally from the trusted registry, then the token's own `decimals()`; a token that resolves neither way is evaluated at 0 decimals, the only value that cannot fail open. For a token outside the registry the RPC is the only source of decimals, and an RPC you do not control could scale the amount and the cap together; pin such tokens with `AGENTWALLET_TOKEN_DECIMALS` (for example `8453:0x<token>=6`) and the pin is used everywhere the RPC's answer would have been.
|
|
46
|
+
`AGENTWALLET_MAX_TX_TOKEN` is the equivalent ceiling for ERC-20 movement, in human units of the token. **Set this one too if you hold stablecoins.** The native cap cannot see a token transfer: an ERC-20 send carries `value = 0` with the amount in the calldata, so `AGENTWALLET_MAX_TX_NATIVE` alone leaves a USDC balance uncapped. `AGENTWALLET_MAX_TX_TOKEN` covers `transfer`, `transferFrom`, `approve`, `increaseAllowance`, EIP-2612 `permit`, Permit2 `approve`, and `deposit()` / `withdraw(uint256)` on the chain's own wrapped-native contract (so `wrap_eth` and `unwrap_eth` keep working under the cap), the approvals because an unbounded allowance is a drain waiting to happen. ERC-721 and ERC-1155 calldata (`setApprovalForAll`, `safeTransferFrom`) is refused while the cap is set, since nothing prices an NFT; without the cap it passes the way an ERC-20 transfer of any size does, except that an operator grant is an unlimited approval and is refused everywhere by default (see `AGENTWALLET_ALLOW_UNLIMITED_APPROVAL`). Any other calldata sent to a token contract or to Permit2 is refused while the cap is set, because the guard cannot price it; set `AGENTWALLET_ALLOW_UNKNOWN_TOKEN_CALLS=1` to allow such calls deliberately. `increaseAllowance` is judged on the resulting total: the current allowance is read first and the call refuses if it cannot be, so two compliant increments cannot stack a standing allowance above the cap. Calls to other contracts (routers, bridges) pass, since they can only pull what an approval already allowed; a contract counts as "other" only when it demonstrably declines `decimals()`, so an absurd or malformed answer, or an RPC that cannot be reached, refuses rather than allows. Decimals are resolved locally from the trusted registry, then the token's own `decimals()`; a token that resolves neither way is evaluated at 0 decimals, the only value that cannot fail open. For a token outside the registry the RPC is the only source of decimals, and an RPC you do not control could scale the amount and the cap together; pin such tokens with `AGENTWALLET_TOKEN_DECIMALS` (for example `8453:0x<token>=6`) and the pin is used everywhere the RPC's answer would have been.
|
|
47
47
|
|
|
48
48
|
### Solana local signing
|
|
49
49
|
|
|
@@ -376,7 +376,7 @@ A per-transaction cap bounds one signature. It cannot see that the five hundredt
|
|
|
376
376
|
|
|
377
377
|
- **`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.
|
|
378
378
|
- **`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) and, in local Solana mode, the rent an SPL send pays to create the recipient's token account (the recipient owns that account and can close it for the lamports; the ledger records it as its own line, method `rent`, to the recipient, and `assets.allow` does not need to list `native` for it).
|
|
379
|
-
- **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, since no budget can count it; grant an exact amount. An NFT operator grant (`setApprovalForAll`, ERC-777 `authorizeOperator`) is unbounded by construction and refused the same way, and an ERC-721 or ERC-1155 transfer is a spend of the collection to the recipient that no budget can price, so it is refused under a dollar budget unless `unpriced_spends` is `"allow"`, with the recipient checked either way. A zero-value call to any other contract (a router, a bridge) reserves nothing: it can only pull what an approval already granted.
|
|
379
|
+
- **Approvals are spends.** An ERC-20 allowance (`approve_token`, `approve_permit2`, `increaseAllowance`, `approveAndCall`, Permit2 `approve`, a signed `permit` in its EIP-2612, DAI or Permit2 shape, 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, since no budget can count it; grant an exact amount. An NFT operator grant (`setApprovalForAll`, ERC-777 `authorizeOperator`) is unbounded by construction and refused the same way, and an ERC-721 or ERC-1155 transfer is a spend of the collection to the recipient that no budget can price, so it is refused under a dollar budget unless `unpriced_spends` is `"allow"`, with the recipient checked either way. A zero-value call to any other contract (a router, a bridge) reserves nothing: it can only pull what an approval already granted. The exception is Permit2 itself: a call to it that the policy cannot price (`permitBatch`, `transferFrom`, `permitTransferFrom`, `lockdown`, nonce invalidation, or an unknown selector) is refused under a policy, since each moves tokens or changes what may be moved.
|
|
380
380
|
- **`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.
|
|
381
381
|
- **`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.
|
|
382
382
|
- **`chains`**, **`assets`**: allow lists; an asset entry can be scoped `"8453:0x..."` or be the word `native`.
|
package/build/index.js
CHANGED
|
@@ -150,6 +150,31 @@ const SPEND_LAYOUTS = {
|
|
|
150
150
|
b88d4fde: { kind: 'token', what: 'ERC-721 safeTransferFrom', toIndex: 1, amountIndex: -1, nft: true }, // safeTransferFrom(address,address,uint256,bytes)
|
|
151
151
|
f242432a: { kind: 'token', what: 'ERC-1155 safeTransferFrom', toIndex: 1, amountIndex: 3, nft: true }, // safeTransferFrom(address,address,uint256,uint256,bytes)
|
|
152
152
|
'2eb2c2d6': { kind: 'token', what: 'ERC-1155 safeBatchTransferFrom', toIndex: 1, amountIndex: 3, nft: true, batch: true }, // safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)
|
|
153
|
+
// Signed allowances (reported privately 2026-11-01, 1.14.9): permit() sets
|
|
154
|
+
// allowance[owner][spender] exactly as approve() does, and the wallet can
|
|
155
|
+
// produce the owner's signature in-process, so one send_transaction both
|
|
156
|
+
// signs and broadcasts a standing allowance. None of these was decoded, so
|
|
157
|
+
// a permit fell through as a zero-value contract call with no budget line,
|
|
158
|
+
// no ledger line, no spender check and no unlimited refusal: the 1.14.1
|
|
159
|
+
// bypass again, through a selector that fix did not cover. The owner word
|
|
160
|
+
// is not compared with the wallet: a permit relayed for another owner is
|
|
161
|
+
// counted as if it were the wallet's own, which over-counts, never under.
|
|
162
|
+
d505accf: { kind: 'approval', what: 'EIP-2612 permit', toIndex: 1, amountIndex: 2 }, // permit(address,address,uint256,uint256,uint8,bytes32,bytes32)
|
|
163
|
+
'8fcbaf0c': { kind: 'approval', what: 'DAI permit', toIndex: 1, amountIndex: 4, operator: true }, // permit(address,address,uint256,uint256,bool,uint8,bytes32,bytes32): allowed=true is unbounded by construction
|
|
164
|
+
'2b67b570': { kind: 'approval', what: 'Permit2 permit', toIndex: 5, amountIndex: 2, tokenIndex: 1, permit2Only: true }, // Permit2.permit(address,((address,uint160,uint48,uint48),address,uint256),bytes): PermitSingle is static, so inlined
|
|
165
|
+
};
|
|
166
|
+
/* The rest of Permit2's surface, by selector: permitBatch, transferFrom (two
|
|
167
|
+
shapes), lockdown, permitTransferFrom and permitWitnessTransferFrom (two
|
|
168
|
+
each), invalidateNonces and invalidateUnorderedNonces. Each moves tokens or
|
|
169
|
+
changes what may be moved, in shapes this table does not price (a batch is
|
|
170
|
+
several allowances in one call), so under a policy such a call to the
|
|
171
|
+
chain's Permit2 is refused rather than let through as a zero-value call to
|
|
172
|
+
"some contract" (1.14.9). The local token cap has refused the same calls
|
|
173
|
+
since the 2026-10-04 audit. */
|
|
174
|
+
const PERMIT2_OTHER = {
|
|
175
|
+
'2a2d80d1': 'permit (batch)', '36c78516': 'transferFrom', '0d58b1db': 'transferFrom (batch)', cc53287f: 'lockdown',
|
|
176
|
+
'30f28b7a': 'permitTransferFrom', edd9444b: 'permitTransferFrom (batch)', '137c29fe': 'permitWitnessTransferFrom', fe8ec1a7: 'permitWitnessTransferFrom (batch)',
|
|
177
|
+
'3ff9dcb1': 'invalidateNonces', '65d9723c': 'invalidateUnorderedNonces',
|
|
153
178
|
};
|
|
154
179
|
const UNBOUNDED_APPROVAL = 1n << 128n;
|
|
155
180
|
const UINT256_MAX = (1n << 256n) - 1n;
|
|
@@ -248,6 +273,13 @@ async function describeSend(body, chainId) {
|
|
|
248
273
|
}
|
|
249
274
|
return { kind: layout.kind, to: counterparty, asset: token, raw: amount.toString(), decimals, ...(carried ? { carried } : {}) };
|
|
250
275
|
}
|
|
276
|
+
// Every priced layout has returned by now; a Permit2 target here is a call
|
|
277
|
+
// the table does not know.
|
|
278
|
+
if (isPermit2 && POLICY.path) {
|
|
279
|
+
const other = PERMIT2_OTHER[data.slice(2, 10).toLowerCase()];
|
|
280
|
+
throw new Error(`policy: calldata selector 0x${data.slice(2, 10)} aimed at Permit2 is ${other ? `Permit2 ${other}` : 'not a Permit2 call this policy can price'}, which moves tokens or changes what may be moved in a shape the policy cannot count; ` +
|
|
281
|
+
`refused rather than treated as a plain contract call. Use Permit2 approve or a single permit, each of which is reserved as an approval. Nothing was signed.`);
|
|
282
|
+
}
|
|
251
283
|
}
|
|
252
284
|
if (data && data !== '0x')
|
|
253
285
|
return { kind: 'contract', to, asset: '', raw: value, decimals: nativeDecimals(chainId) };
|
|
@@ -1255,7 +1287,7 @@ function resolveChainId(network) {
|
|
|
1255
1287
|
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');
|
|
1256
1288
|
const server = new McpServer({
|
|
1257
1289
|
name: 'agentwallet',
|
|
1258
|
-
version: '1.14.
|
|
1290
|
+
version: '1.14.9',
|
|
1259
1291
|
}, {
|
|
1260
1292
|
instructions: `AgentWallet gives AI agents their own blockchain wallets. ${anyLocalMode()
|
|
1261
1293
|
? '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.'
|
package/build/local-wallet.js
CHANGED
|
@@ -351,6 +351,13 @@ const CAP_LAYOUTS = {
|
|
|
351
351
|
'9bd9bbc6': { amountIndex: 1, what: 'token transfer (ERC-777 send)' }, // send(address,uint256,bytes)
|
|
352
352
|
'62ad1b83': { amountIndex: 2, what: 'token transfer (ERC-777 operatorSend)' }, // operatorSend(address,address,uint256,bytes,bytes)
|
|
353
353
|
'87517c45': { amountIndex: 2, tokenIndex: 0, what: 'Permit2 approval' }, // Permit2.approve(address,address,uint160,uint48)
|
|
354
|
+
// EIP-2612 permit sets the allowance exactly as approve does, with the
|
|
355
|
+
// owner's signature in the calldata (the local key can produce it), so it
|
|
356
|
+
// is priced as an approval (1.14.9, reported privately 2026-11-01). The DAI
|
|
357
|
+
// permit (0x8fcbaf0c, a bool grant) and Permit2.permit stay unpriced here
|
|
358
|
+
// and are refused by the rule below, as every unpriced call to a token or to
|
|
359
|
+
// Permit2 is; the spend policy classifies both.
|
|
360
|
+
d505accf: { amountIndex: 2, what: 'approval (EIP-2612 permit)' }, // permit(address,address,uint256,uint256,uint8,bytes32,bytes32)
|
|
354
361
|
// WETH9 calls, priced only when `to` is the chain's own wrapped-native
|
|
355
362
|
// contract (wrappedNativeOnly). deposit() carries its amount as msg.value,
|
|
356
363
|
// not in calldata, hence amountIndex -1; withdraw(uint256) has it at word 0.
|
|
@@ -404,7 +411,7 @@ async function assertWithinTokenCap(chainId, to, data, valueWei = 0n) {
|
|
|
404
411
|
? `the RPC for chain ${chainId} could not be reached to classify the target, so the guard cannot tell a token from a router`
|
|
405
412
|
: `the target is ${isPermit2 ? 'Permit2' : 'a token contract'} and this calldata is not one AGENTWALLET_MAX_TX_TOKEN can price`;
|
|
406
413
|
throw new Error(`Blocked by local guard: calldata selector 0x${selector} refused rather than let through uncapped: ${why}. ` +
|
|
407
|
-
`Use transfer, transferFrom, approve, increaseAllowance, Permit2 approve, ` +
|
|
414
|
+
`Use transfer, transferFrom, approve, increaseAllowance, EIP-2612 permit, Permit2 approve, ` +
|
|
408
415
|
`or deposit/withdraw on the chain's wrapped-native contract, ` +
|
|
409
416
|
`or ask the operator, who alone can set AGENTWALLET_ALLOW_UNKNOWN_TOKEN_CALLS=1.`);
|
|
410
417
|
}
|
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.9",
|
|
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",
|