agentwallet-mcp 1.14.11 → 1.14.12
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 +1 -1
- package/build/index.js +1 -1
- package/build/ledger.d.ts +22 -1
- package/build/ledger.js +84 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -387,7 +387,7 @@ A per-transaction cap bounds one signature. It cannot see that the five hundredt
|
|
|
387
387
|
|
|
388
388
|
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.
|
|
389
389
|
|
|
390
|
-
**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 or answered with a 5xx, any host answer that carries a transaction hash) is kept counted as `unknown` rather than voided; a failure before anything left this process, including a policy or local-mode refusal, or a 4xx refusal from the host, is always voided and never consumes budget. A hosted send is forwarded with the chain the policy priced it on (`chain_id`, else `AGENTWALLET_CHAIN_ID`, else 8453), and a custodian answer naming another chain is written onto the ledger line as a mismatch. 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.
|
|
390
|
+
**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 or answered with a 5xx, any host answer that carries a transaction hash) is kept counted as `unknown` rather than voided; a failure before anything left this process, including a policy or local-mode refusal, or a 4xx refusal from the host, is always voided and never consumes budget. A hosted send is forwarded with the chain the policy priced it on (`chain_id`, else `AGENTWALLET_CHAIN_ID`, else 8453), and a custodian answer naming another chain is written onto the ledger line as a mismatch. 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 and reclaimed in one step through `<ledger>.lock.reclaim`, and a release removes only the lock it took. 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.
|
|
391
391
|
|
|
392
392
|
### Encrypted keyfiles
|
|
393
393
|
|
package/build/index.js
CHANGED
|
@@ -1399,7 +1399,7 @@ function resolveChainId(network) {
|
|
|
1399
1399
|
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');
|
|
1400
1400
|
const server = new McpServer({
|
|
1401
1401
|
name: 'agentwallet',
|
|
1402
|
-
version: '1.14.
|
|
1402
|
+
version: '1.14.12',
|
|
1403
1403
|
}, {
|
|
1404
1404
|
instructions: `AgentWallet gives AI agents their own blockchain wallets. ${anyLocalMode()
|
|
1405
1405
|
? '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/ledger.d.ts
CHANGED
|
@@ -102,8 +102,29 @@ export declare class Ledger {
|
|
|
102
102
|
* line; otherwise it was rewritten underneath us and nothing is adopted.
|
|
103
103
|
*/
|
|
104
104
|
private sync;
|
|
105
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* Take the exclusive lock file; returns the release. A lock a dead process
|
|
107
|
+
* left behind is cleared after LOCK_STALE_MS (reclaimStale). The release
|
|
108
|
+
* removes the lock only while it still carries this call's token: a lock
|
|
109
|
+
* this process held past the stale window and another process reclaimed
|
|
110
|
+
* belongs to whoever took it next, and must not be removed from under them.
|
|
111
|
+
*/
|
|
106
112
|
private lock;
|
|
113
|
+
/**
|
|
114
|
+
* Clear a lock left by a process that died holding it. The judgement that
|
|
115
|
+
* the lock is stale and its removal are one step under a second file, the
|
|
116
|
+
* claim, taken with 'wx' like the lock itself: a waiter that cannot take the
|
|
117
|
+
* claim leaves the lock alone and retries, and the one that holds it looks
|
|
118
|
+
* at the lock again (mtime and first line) before removing it, so a fresh
|
|
119
|
+
* lock another waiter took in the meantime is never the one removed. Before
|
|
120
|
+
* 1.14.12 the stat and the unlink were two calls with nothing between them,
|
|
121
|
+
* and two waiters judging one stale lock could, between them, remove the
|
|
122
|
+
* fresh lock one of them had just taken, after which both held the ledger
|
|
123
|
+
* (reported privately, 2026-11-02). A claim older than the stale window was
|
|
124
|
+
* itself left by a waiter that died inside this step and is removed the old
|
|
125
|
+
* way; the step is a stat and an unlink, so that is the window that remains.
|
|
126
|
+
*/
|
|
127
|
+
private reclaimStale;
|
|
107
128
|
private append;
|
|
108
129
|
private notify;
|
|
109
130
|
/** Spends not voided, newest last. */
|
package/build/ledger.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* file that no longer holds what this process wrote is refused the same way.
|
|
18
18
|
*/
|
|
19
19
|
import { appendFileSync, existsSync, readFileSync, openSync, closeSync, writeSync, unlinkSync, statSync } from 'node:fs';
|
|
20
|
-
import { createHash, createHmac } from 'node:crypto';
|
|
20
|
+
import { createHash, createHmac, randomBytes } from 'node:crypto';
|
|
21
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;
|
|
@@ -25,6 +25,17 @@ const MAX_LINE = 16 * 1024;
|
|
|
25
25
|
const LOCK_WAIT_MS = 10_000;
|
|
26
26
|
/** A lock older than this was left by a process that died holding it. */
|
|
27
27
|
const LOCK_STALE_MS = 30_000;
|
|
28
|
+
/** What a lock file holds: the holder's pid and a random token, so a release can tell its own lock from one taken after it. */
|
|
29
|
+
function lockToken() { return `${process.pid} ${randomBytes(8).toString('hex')}`; }
|
|
30
|
+
/** The lock file as one thing to compare: its mtime and its first line. null when it is gone. */
|
|
31
|
+
function lockIdentity(lockPath) {
|
|
32
|
+
try {
|
|
33
|
+
return { mtimeMs: statSync(lockPath).mtimeMs, line: readFileSync(lockPath, 'utf8').split(/\r?\n/)[0] };
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
28
39
|
const sleeper = new Int32Array(new SharedArrayBuffer(4));
|
|
29
40
|
function sleepSync(ms) { Atomics.wait(sleeper, 0, 0, ms); }
|
|
30
41
|
function hashOf(prev, body) {
|
|
@@ -105,43 +116,101 @@ export class Ledger {
|
|
|
105
116
|
this.last = c.last;
|
|
106
117
|
this.integrity = c.integrity;
|
|
107
118
|
}
|
|
108
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* Take the exclusive lock file; returns the release. A lock a dead process
|
|
121
|
+
* left behind is cleared after LOCK_STALE_MS (reclaimStale). The release
|
|
122
|
+
* removes the lock only while it still carries this call's token: a lock
|
|
123
|
+
* this process held past the stale window and another process reclaimed
|
|
124
|
+
* belongs to whoever took it next, and must not be removed from under them.
|
|
125
|
+
*/
|
|
109
126
|
lock() {
|
|
110
127
|
if (!this.path)
|
|
111
128
|
return () => { };
|
|
112
129
|
const lockPath = this.path + '.lock';
|
|
130
|
+
const token = lockToken();
|
|
113
131
|
const deadline = Date.now() + this.lockWaitMs;
|
|
114
132
|
for (;;) {
|
|
115
133
|
try {
|
|
116
134
|
const fd = openSync(lockPath, 'wx', 0o600);
|
|
117
135
|
try {
|
|
118
|
-
writeSync(fd, `${
|
|
136
|
+
writeSync(fd, `${token} ${new Date().toISOString()}\n`);
|
|
119
137
|
}
|
|
120
138
|
finally {
|
|
121
139
|
closeSync(fd);
|
|
122
140
|
}
|
|
123
|
-
return () => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
141
|
+
return () => {
|
|
142
|
+
if (!lockIdentity(lockPath)?.line.startsWith(token + ' '))
|
|
143
|
+
return; // reclaimed as stale and retaken, or already gone
|
|
144
|
+
try {
|
|
145
|
+
unlinkSync(lockPath);
|
|
146
|
+
}
|
|
147
|
+
catch { /* already gone */ }
|
|
148
|
+
};
|
|
127
149
|
}
|
|
128
150
|
catch (e) {
|
|
129
151
|
if (e.code !== 'EEXIST')
|
|
130
152
|
throw e;
|
|
131
153
|
}
|
|
154
|
+
this.reclaimStale(lockPath);
|
|
155
|
+
if (Date.now() >= deadline) {
|
|
156
|
+
throw new Error(`The ledger at ${this.path} is locked by another agentwallet-mcp process (${lockPath}) that did not finish within ${this.lockWaitMs} ms. Nothing was signed. If no other process is running, remove the lock file.`);
|
|
157
|
+
}
|
|
158
|
+
sleepSync(5);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Clear a lock left by a process that died holding it. The judgement that
|
|
163
|
+
* the lock is stale and its removal are one step under a second file, the
|
|
164
|
+
* claim, taken with 'wx' like the lock itself: a waiter that cannot take the
|
|
165
|
+
* claim leaves the lock alone and retries, and the one that holds it looks
|
|
166
|
+
* at the lock again (mtime and first line) before removing it, so a fresh
|
|
167
|
+
* lock another waiter took in the meantime is never the one removed. Before
|
|
168
|
+
* 1.14.12 the stat and the unlink were two calls with nothing between them,
|
|
169
|
+
* and two waiters judging one stale lock could, between them, remove the
|
|
170
|
+
* fresh lock one of them had just taken, after which both held the ledger
|
|
171
|
+
* (reported privately, 2026-11-02). A claim older than the stale window was
|
|
172
|
+
* itself left by a waiter that died inside this step and is removed the old
|
|
173
|
+
* way; the step is a stat and an unlink, so that is the window that remains.
|
|
174
|
+
*/
|
|
175
|
+
reclaimStale(lockPath) {
|
|
176
|
+
const seen = lockIdentity(lockPath);
|
|
177
|
+
if (!seen || Date.now() - seen.mtimeMs <= LOCK_STALE_MS)
|
|
178
|
+
return; // gone, or live
|
|
179
|
+
const claimPath = lockPath + '.reclaim';
|
|
180
|
+
let fd;
|
|
181
|
+
try {
|
|
182
|
+
fd = openSync(claimPath, 'wx', 0o600);
|
|
183
|
+
}
|
|
184
|
+
catch (e) {
|
|
185
|
+
if (e.code !== 'EEXIST')
|
|
186
|
+
throw e;
|
|
187
|
+
try {
|
|
188
|
+
if (Date.now() - statSync(claimPath).mtimeMs > LOCK_STALE_MS)
|
|
189
|
+
unlinkSync(claimPath);
|
|
190
|
+
}
|
|
191
|
+
catch { /* finished or cleared meanwhile */ }
|
|
192
|
+
return; // another waiter is clearing it; retry the lock
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
132
195
|
try {
|
|
133
|
-
|
|
196
|
+
writeSync(fd, `${lockToken()} ${new Date().toISOString()}\n`);
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
closeSync(fd);
|
|
200
|
+
}
|
|
201
|
+
const now = lockIdentity(lockPath);
|
|
202
|
+
if (now && now.mtimeMs === seen.mtimeMs && now.line === seen.line && Date.now() - now.mtimeMs > LOCK_STALE_MS) {
|
|
203
|
+
try {
|
|
134
204
|
unlinkSync(lockPath);
|
|
135
|
-
continue;
|
|
136
205
|
}
|
|
206
|
+
catch { /* gone meanwhile */ }
|
|
137
207
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
throw new Error(`The ledger at ${this.path} is locked by another agentwallet-mcp process (${lockPath}) that did not finish within ${this.lockWaitMs} ms. Nothing was signed. If no other process is running, remove the lock file.`);
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
try {
|
|
211
|
+
unlinkSync(claimPath);
|
|
143
212
|
}
|
|
144
|
-
|
|
213
|
+
catch { /* already gone */ }
|
|
145
214
|
}
|
|
146
215
|
}
|
|
147
216
|
append(body) {
|
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.12",
|
|
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",
|