@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.
Files changed (3) hide show
  1. package/README.md +12 -0
  2. package/dist/index.js +129 -0
  3. 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');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aspect-warden/mcp-server",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "warden-mcp": "dist/index.js"