@aspect-warden/mcp-server 0.3.2 → 0.4.1
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 +12 -0
- package/dist/index.js +129 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,6 +51,7 @@ npx skills add tetherto/wdk-agent-skills
|
|
|
51
51
|
| `POLICY_DELEGATE_ADDRESS` | No | — | Deployed PolicyDelegate contract for EIP-7702 |
|
|
52
52
|
| `SEPOLIA_USDT_ADDRESS` | No | `0x7169...BA06` | USDT token address on Sepolia |
|
|
53
53
|
| `ERC8004_IDENTITY_REGISTRY` | No | — | ERC-8004 identity registry address |
|
|
54
|
+
| `WARDEN_STORAGE_KEY` | No | machine-scoped | Passphrase for encrypting persisted wallet state |
|
|
54
55
|
|
|
55
56
|
## MCP Tools
|
|
56
57
|
|
|
@@ -123,6 +124,17 @@ Agent: [calls warden_transfer — PolicyEngine blocks it]
|
|
|
123
124
|
Current daily spend: 5 USDT.
|
|
124
125
|
```
|
|
125
126
|
|
|
127
|
+
## Wallet Persistence
|
|
128
|
+
|
|
129
|
+
Wallet private keys and policy configuration are automatically saved to `~/.warden/wallet-state.enc` using AES-256-CBC encryption. When the MCP server restarts (e.g., Claude Desktop relaunch), it restores the previous wallet — no need to call `warden_create_wallet` again.
|
|
130
|
+
|
|
131
|
+
- **Storage**: `~/.warden/wallet-state.enc` (encrypted, file permissions `0600`)
|
|
132
|
+
- **Encryption**: AES-256-CBC with key derived from `WARDEN_STORAGE_KEY` env var (or machine-scoped default)
|
|
133
|
+
- **What's persisted**: private key, agent ID, policy configuration
|
|
134
|
+
- **What's NOT persisted**: spending counters, audit logs, session keys (reset on restart)
|
|
135
|
+
|
|
136
|
+
To use a different wallet, simply call `warden_create_wallet` again — it overwrites the saved state.
|
|
137
|
+
|
|
126
138
|
## Architecture
|
|
127
139
|
|
|
128
140
|
```
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,10 @@ import { z } from 'zod';
|
|
|
5
5
|
import { createPublicClient, createWalletClient, http, parseAbi, encodeFunctionData, formatEther, formatUnits, parseUnits, } from 'viem';
|
|
6
6
|
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
|
|
7
7
|
import { sepolia } from 'viem/chains';
|
|
8
|
+
import { createHash, createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
|
|
9
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
8
12
|
// ============================================================
|
|
9
13
|
// Configuration from environment
|
|
10
14
|
// ============================================================
|
|
@@ -34,6 +38,60 @@ const ERC8004_ABI = parseAbi([
|
|
|
34
38
|
]);
|
|
35
39
|
// PolicyEngine and AuditLogger imported from @aspect-warden/policy-engine
|
|
36
40
|
// ============================================================
|
|
41
|
+
// Encrypted Wallet Persistence
|
|
42
|
+
// ============================================================
|
|
43
|
+
const STORAGE_DIR = join(homedir(), '.warden');
|
|
44
|
+
const STATE_FILE = join(STORAGE_DIR, 'wallet-state.enc');
|
|
45
|
+
// Derive a 32-byte AES key from a passphrase
|
|
46
|
+
function deriveKey(passphrase) {
|
|
47
|
+
return createHash('sha256').update(passphrase).digest();
|
|
48
|
+
}
|
|
49
|
+
function encrypt(data, passphrase) {
|
|
50
|
+
const key = deriveKey(passphrase);
|
|
51
|
+
const iv = randomBytes(16);
|
|
52
|
+
const cipher = createCipheriv('aes-256-cbc', key, iv);
|
|
53
|
+
let encrypted = cipher.update(data, 'utf8', 'hex');
|
|
54
|
+
encrypted += cipher.final('hex');
|
|
55
|
+
return iv.toString('hex') + ':' + encrypted;
|
|
56
|
+
}
|
|
57
|
+
function decrypt(payload, passphrase) {
|
|
58
|
+
const key = deriveKey(passphrase);
|
|
59
|
+
const [ivHex, encrypted] = payload.split(':');
|
|
60
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
61
|
+
const decipher = createDecipheriv('aes-256-cbc', key, iv);
|
|
62
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
63
|
+
decrypted += decipher.final('utf8');
|
|
64
|
+
return decrypted;
|
|
65
|
+
}
|
|
66
|
+
const STORAGE_KEY = process.env.WARDEN_STORAGE_KEY || `warden-${homedir()}`;
|
|
67
|
+
function saveState(state) {
|
|
68
|
+
try {
|
|
69
|
+
if (!existsSync(STORAGE_DIR)) {
|
|
70
|
+
mkdirSync(STORAGE_DIR, { recursive: true, mode: 0o700 });
|
|
71
|
+
}
|
|
72
|
+
const json = JSON.stringify(state);
|
|
73
|
+
const encrypted = encrypt(json, STORAGE_KEY);
|
|
74
|
+
writeFileSync(STATE_FILE, encrypted, { mode: 0o600 });
|
|
75
|
+
console.error('[Warden MCP] Wallet state saved to ~/.warden/wallet-state.enc');
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
console.error('[Warden MCP] Failed to save wallet state:', err);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function loadState() {
|
|
82
|
+
try {
|
|
83
|
+
if (!existsSync(STATE_FILE))
|
|
84
|
+
return null;
|
|
85
|
+
const encrypted = readFileSync(STATE_FILE, 'utf8');
|
|
86
|
+
const json = decrypt(encrypted, STORAGE_KEY);
|
|
87
|
+
return JSON.parse(json);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
console.error('[Warden MCP] Failed to load wallet state (may be from different key):', err);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// ============================================================
|
|
37
95
|
// MCP Server State
|
|
38
96
|
// ============================================================
|
|
39
97
|
let policyEngine = null;
|
|
@@ -114,6 +172,22 @@ server.tool('warden_create_wallet', 'Create a new policy-enforced wallet for the
|
|
|
114
172
|
policyEngine = new PolicyEngine(policy);
|
|
115
173
|
auditLogger = new AuditLogger({ maxEntries: 10000 });
|
|
116
174
|
frozen = false;
|
|
175
|
+
// Persist wallet state for recovery across restarts
|
|
176
|
+
saveState({
|
|
177
|
+
privateKey: storedPrivateKey,
|
|
178
|
+
agentId,
|
|
179
|
+
policy: {
|
|
180
|
+
maxPerTx: policy.maxPerTx.toString(),
|
|
181
|
+
dailyLimit: policy.dailyLimit.toString(),
|
|
182
|
+
requireApprovalAbove: policy.requireApprovalAbove.toString(),
|
|
183
|
+
cooldownMs: policy.cooldownMs,
|
|
184
|
+
allowedTokens: policy.allowedTokens,
|
|
185
|
+
blockedTokens: policy.blockedTokens,
|
|
186
|
+
allowedRecipients: policy.allowedRecipients,
|
|
187
|
+
blockedRecipients: policy.blockedRecipients,
|
|
188
|
+
allowedChains: policy.allowedChains,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
117
191
|
return {
|
|
118
192
|
content: [{
|
|
119
193
|
type: 'text',
|
|
@@ -248,6 +322,7 @@ server.tool('warden_transfer', 'Send ERC-20 tokens with policy enforcement. Eval
|
|
|
248
322
|
const decision = policyEngine.evaluate(recipient, valueMicro, tokenAddress, 'ethereum');
|
|
249
323
|
auditLogger.log(decision);
|
|
250
324
|
if (!decision.approved) {
|
|
325
|
+
const currentPolicy = policyEngine.getPolicy();
|
|
251
326
|
return {
|
|
252
327
|
content: [{
|
|
253
328
|
type: 'text',
|
|
@@ -257,6 +332,10 @@ server.tool('warden_transfer', 'Send ERC-20 tokens with policy enforcement. Eval
|
|
|
257
332
|
reason: decision.reason,
|
|
258
333
|
ruleTriggered: decision.ruleTriggered,
|
|
259
334
|
riskScore: decision.riskScore,
|
|
335
|
+
currentLimits: {
|
|
336
|
+
maxPerTx: `${Number(currentPolicy.maxPerTx) / 1e6} USDT`,
|
|
337
|
+
dailyLimit: `${Number(currentPolicy.dailyLimit) / 1e6} USDT`,
|
|
338
|
+
},
|
|
260
339
|
}),
|
|
261
340
|
}],
|
|
262
341
|
};
|
|
@@ -412,6 +491,25 @@ server.tool('warden_update_policy', 'Modify policy rules at runtime. Can update
|
|
|
412
491
|
updatedFields.push('cooldownMs');
|
|
413
492
|
}
|
|
414
493
|
policyEngine.updatePolicy(updates);
|
|
494
|
+
// Persist updated policy so it survives restarts
|
|
495
|
+
if (storedPrivateKey) {
|
|
496
|
+
const p = policyEngine.getPolicy();
|
|
497
|
+
saveState({
|
|
498
|
+
privateKey: storedPrivateKey,
|
|
499
|
+
agentId: p.agentId,
|
|
500
|
+
policy: {
|
|
501
|
+
maxPerTx: p.maxPerTx.toString(),
|
|
502
|
+
dailyLimit: p.dailyLimit.toString(),
|
|
503
|
+
requireApprovalAbove: p.requireApprovalAbove.toString(),
|
|
504
|
+
cooldownMs: p.cooldownMs,
|
|
505
|
+
allowedTokens: p.allowedTokens,
|
|
506
|
+
blockedTokens: p.blockedTokens,
|
|
507
|
+
allowedRecipients: p.allowedRecipients,
|
|
508
|
+
blockedRecipients: p.blockedRecipients,
|
|
509
|
+
allowedChains: p.allowedChains,
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
}
|
|
415
513
|
return {
|
|
416
514
|
content: [{
|
|
417
515
|
type: 'text',
|
|
@@ -1034,6 +1132,37 @@ server.tool('warden_get_permissions', 'Query current permissions granted to an A
|
|
|
1034
1132
|
// Main -- stdio transport
|
|
1035
1133
|
// ============================================================
|
|
1036
1134
|
async function main() {
|
|
1135
|
+
// Restore wallet state from previous session if available
|
|
1136
|
+
const saved = loadState();
|
|
1137
|
+
if (saved) {
|
|
1138
|
+
try {
|
|
1139
|
+
initializeClients(saved.privateKey);
|
|
1140
|
+
const restoredPolicy = {
|
|
1141
|
+
agentId: saved.agentId,
|
|
1142
|
+
maxPerTx: BigInt(saved.policy.maxPerTx),
|
|
1143
|
+
dailyLimit: BigInt(saved.policy.dailyLimit),
|
|
1144
|
+
requireApprovalAbove: BigInt(saved.policy.requireApprovalAbove),
|
|
1145
|
+
cooldownMs: saved.policy.cooldownMs,
|
|
1146
|
+
allowedTokens: saved.policy.allowedTokens,
|
|
1147
|
+
blockedTokens: saved.policy.blockedTokens,
|
|
1148
|
+
allowedRecipients: saved.policy.allowedRecipients,
|
|
1149
|
+
blockedRecipients: saved.policy.blockedRecipients,
|
|
1150
|
+
allowedChains: saved.policy.allowedChains,
|
|
1151
|
+
anomalyDetection: {
|
|
1152
|
+
maxTxPerHour: 20,
|
|
1153
|
+
maxRecipientsPerHour: 5,
|
|
1154
|
+
largeTransactionPct: 50,
|
|
1155
|
+
},
|
|
1156
|
+
};
|
|
1157
|
+
policyEngine = new PolicyEngine(restoredPolicy);
|
|
1158
|
+
auditLogger = new AuditLogger({ maxEntries: 10000 });
|
|
1159
|
+
frozen = false;
|
|
1160
|
+
console.error(`[Warden MCP] Restored wallet ${walletAddress} (agent: ${saved.agentId})`);
|
|
1161
|
+
}
|
|
1162
|
+
catch (err) {
|
|
1163
|
+
console.error('[Warden MCP] Failed to restore wallet state:', err);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1037
1166
|
const transport = new StdioServerTransport();
|
|
1038
1167
|
await server.connect(transport);
|
|
1039
1168
|
console.error('[Warden MCP] Server running on stdio');
|