@aspect-warden/mcp-server 0.3.2 → 0.4.0

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 +105 -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',
@@ -1034,6 +1108,37 @@ server.tool('warden_get_permissions', 'Query current permissions granted to an A
1034
1108
  // Main -- stdio transport
1035
1109
  // ============================================================
1036
1110
  async function main() {
1111
+ // Restore wallet state from previous session if available
1112
+ const saved = loadState();
1113
+ if (saved) {
1114
+ try {
1115
+ initializeClients(saved.privateKey);
1116
+ const restoredPolicy = {
1117
+ agentId: saved.agentId,
1118
+ maxPerTx: BigInt(saved.policy.maxPerTx),
1119
+ dailyLimit: BigInt(saved.policy.dailyLimit),
1120
+ requireApprovalAbove: BigInt(saved.policy.requireApprovalAbove),
1121
+ cooldownMs: saved.policy.cooldownMs,
1122
+ allowedTokens: saved.policy.allowedTokens,
1123
+ blockedTokens: saved.policy.blockedTokens,
1124
+ allowedRecipients: saved.policy.allowedRecipients,
1125
+ blockedRecipients: saved.policy.blockedRecipients,
1126
+ allowedChains: saved.policy.allowedChains,
1127
+ anomalyDetection: {
1128
+ maxTxPerHour: 20,
1129
+ maxRecipientsPerHour: 5,
1130
+ largeTransactionPct: 50,
1131
+ },
1132
+ };
1133
+ policyEngine = new PolicyEngine(restoredPolicy);
1134
+ auditLogger = new AuditLogger({ maxEntries: 10000 });
1135
+ frozen = false;
1136
+ console.error(`[Warden MCP] Restored wallet ${walletAddress} (agent: ${saved.agentId})`);
1137
+ }
1138
+ catch (err) {
1139
+ console.error('[Warden MCP] Failed to restore wallet state:', err);
1140
+ }
1141
+ }
1037
1142
  const transport = new StdioServerTransport();
1038
1143
  await server.connect(transport);
1039
1144
  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.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "warden-mcp": "dist/index.js"