@darksol/terminal 0.1.0 → 0.2.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.
- package/README.md +99 -0
- package/package.json +12 -3
- package/src/cli.js +159 -0
- package/src/config/keys.js +320 -0
- package/src/llm/engine.js +286 -0
- package/src/llm/intent.js +310 -0
- package/src/ui/banner.js +4 -2
- package/src/utils/helpers.js +677 -0
- package/src/wallet/agent-signer.js +556 -0
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
import { ethers } from 'ethers';
|
|
2
|
+
import { createServer } from 'http';
|
|
3
|
+
import { randomBytes } from 'crypto';
|
|
4
|
+
import { decryptKey, loadWallet, walletExists } from './keystore.js';
|
|
5
|
+
import { getConfig, getRPC } from '../config/store.js';
|
|
6
|
+
import { theme } from '../ui/theme.js';
|
|
7
|
+
import { spinner, kvDisplay, success, error, warn, info } from '../ui/components.js';
|
|
8
|
+
import { showSection, showDivider } from '../ui/banner.js';
|
|
9
|
+
import inquirer from 'inquirer';
|
|
10
|
+
|
|
11
|
+
// ══════════════════════════════════════════════════
|
|
12
|
+
// DARKSOL AGENT SIGNER
|
|
13
|
+
// ══════════════════════════════════════════════════
|
|
14
|
+
//
|
|
15
|
+
// Problem: AI agents (OpenClaw, etc.) need to sign transactions, but
|
|
16
|
+
// exposing private keys to LLMs is dangerous — prompt injection could
|
|
17
|
+
// leak the key. Existing wallets (Bankr, Phantom MCP) don't support
|
|
18
|
+
// x402 payments or real contract signing.
|
|
19
|
+
//
|
|
20
|
+
// Solution: A local signing proxy that:
|
|
21
|
+
// 1. Decrypts the private key in memory ONCE at startup
|
|
22
|
+
// 2. Exposes a REST API for signing operations
|
|
23
|
+
// 3. NEVER returns the private key through any endpoint
|
|
24
|
+
// 4. Validates every transaction before signing
|
|
25
|
+
// 5. Supports spending limits, allowlisted contracts, and operation types
|
|
26
|
+
// 6. Auth via one-time bearer token (generated at startup, shown only in terminal)
|
|
27
|
+
//
|
|
28
|
+
// The agent gets: address, chainId, sign capabilities
|
|
29
|
+
// The agent NEVER gets: private key, mnemonic, keystore
|
|
30
|
+
//
|
|
31
|
+
// ══════════════════════════════════════════════════
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Security policy for the agent signer
|
|
35
|
+
*/
|
|
36
|
+
const DEFAULT_POLICY = {
|
|
37
|
+
// Max ETH value per transaction (in ETH)
|
|
38
|
+
maxValuePerTx: 1.0,
|
|
39
|
+
|
|
40
|
+
// Max gas price multiplier (prevents gas drain attacks)
|
|
41
|
+
maxGasMultiplier: 3.0,
|
|
42
|
+
|
|
43
|
+
// Allowed operations
|
|
44
|
+
allowedOps: ['sign_transaction', 'sign_message', 'sign_typed_data', 'get_address', 'get_balance', 'get_chain'],
|
|
45
|
+
|
|
46
|
+
// Contract allowlist (empty = allow all, populated = only these)
|
|
47
|
+
allowlistedContracts: [],
|
|
48
|
+
|
|
49
|
+
// Blocked selectors (known dangerous: transferOwnership, selfdestruct, etc.)
|
|
50
|
+
blockedSelectors: [
|
|
51
|
+
'0xf2fde38b', // transferOwnership(address)
|
|
52
|
+
'0x715018a6', // renounceOwnership()
|
|
53
|
+
'0x00000000', // fallback (raw ETH send blocked by default)
|
|
54
|
+
],
|
|
55
|
+
|
|
56
|
+
// Daily spending limit (in ETH equivalent)
|
|
57
|
+
dailySpendLimit: 5.0,
|
|
58
|
+
|
|
59
|
+
// Require human confirmation for txs above this value (in ETH)
|
|
60
|
+
confirmAbove: 0.5,
|
|
61
|
+
|
|
62
|
+
// Log all operations
|
|
63
|
+
auditLog: true,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The Agent Signer — PK-isolated transaction signing service
|
|
68
|
+
*/
|
|
69
|
+
export class AgentSigner {
|
|
70
|
+
constructor(walletName, password, opts = {}) {
|
|
71
|
+
this.walletName = walletName;
|
|
72
|
+
this.password = password;
|
|
73
|
+
this.policy = { ...DEFAULT_POLICY, ...opts.policy };
|
|
74
|
+
this.signer = null;
|
|
75
|
+
this.provider = null;
|
|
76
|
+
this.address = null;
|
|
77
|
+
this.chain = null;
|
|
78
|
+
this.server = null;
|
|
79
|
+
this.bearerToken = null;
|
|
80
|
+
this.dailySpent = 0;
|
|
81
|
+
this.dailyResetTime = Date.now();
|
|
82
|
+
this.auditLog = [];
|
|
83
|
+
this.port = opts.port || 18790;
|
|
84
|
+
this.host = opts.host || '127.0.0.1'; // Loopback only!
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Initialize — decrypt key and create signer (key stays in memory only)
|
|
89
|
+
*/
|
|
90
|
+
async init() {
|
|
91
|
+
if (!walletExists(this.walletName)) {
|
|
92
|
+
throw new Error(`Wallet "${this.walletName}" not found`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const walletData = loadWallet(this.walletName);
|
|
96
|
+
const privateKey = decryptKey(walletData.keystore, this.password);
|
|
97
|
+
this.chain = walletData.chain || getConfig('chain') || 'base';
|
|
98
|
+
const rpcUrl = getRPC(this.chain);
|
|
99
|
+
this.provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
100
|
+
this.signer = new ethers.Wallet(privateKey, this.provider);
|
|
101
|
+
this.address = this.signer.address;
|
|
102
|
+
|
|
103
|
+
// Generate one-time bearer token
|
|
104
|
+
this.bearerToken = randomBytes(32).toString('hex');
|
|
105
|
+
|
|
106
|
+
// The private key variable goes out of scope here
|
|
107
|
+
// It only lives in the ethers.Wallet instance (this.signer)
|
|
108
|
+
// There is no API endpoint that returns it
|
|
109
|
+
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Validate a transaction against the security policy
|
|
115
|
+
*/
|
|
116
|
+
validateTransaction(tx) {
|
|
117
|
+
const errors = [];
|
|
118
|
+
|
|
119
|
+
// Check value limit
|
|
120
|
+
if (tx.value) {
|
|
121
|
+
const valueETH = parseFloat(ethers.formatEther(tx.value));
|
|
122
|
+
if (valueETH > this.policy.maxValuePerTx) {
|
|
123
|
+
errors.push(`Value ${valueETH} ETH exceeds limit of ${this.policy.maxValuePerTx} ETH`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check daily limit
|
|
128
|
+
const txValue = tx.value ? parseFloat(ethers.formatEther(tx.value)) : 0;
|
|
129
|
+
if (this.dailySpent + txValue > this.policy.dailySpendLimit) {
|
|
130
|
+
errors.push(`Daily spend limit reached (${this.policy.dailySpendLimit} ETH)`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check contract allowlist
|
|
134
|
+
if (this.policy.allowlistedContracts.length > 0 && tx.to) {
|
|
135
|
+
if (!this.policy.allowlistedContracts.includes(tx.to.toLowerCase())) {
|
|
136
|
+
errors.push(`Contract ${tx.to} not in allowlist`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check blocked selectors
|
|
141
|
+
if (tx.data && tx.data.length >= 10) {
|
|
142
|
+
const selector = tx.data.slice(0, 10).toLowerCase();
|
|
143
|
+
if (this.policy.blockedSelectors.includes(selector)) {
|
|
144
|
+
errors.push(`Function selector ${selector} is blocked by security policy`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check gas limits
|
|
149
|
+
if (tx.maxFeePerGas) {
|
|
150
|
+
// Will be validated against current gas price at sign time
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return errors;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Sign a transaction (with policy checks)
|
|
158
|
+
*/
|
|
159
|
+
async signTransaction(tx) {
|
|
160
|
+
// Reset daily counter if needed
|
|
161
|
+
if (Date.now() - this.dailyResetTime > 86400000) {
|
|
162
|
+
this.dailySpent = 0;
|
|
163
|
+
this.dailyResetTime = Date.now();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const errors = this.validateTransaction(tx);
|
|
167
|
+
if (errors.length > 0) {
|
|
168
|
+
this._log('sign_transaction', 'DENIED', { errors, tx: this._sanitizeTx(tx) });
|
|
169
|
+
throw new Error(`Transaction blocked: ${errors.join('; ')}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Sign
|
|
173
|
+
const signedTx = await this.signer.signTransaction(tx);
|
|
174
|
+
|
|
175
|
+
// Track spending
|
|
176
|
+
if (tx.value) {
|
|
177
|
+
this.dailySpent += parseFloat(ethers.formatEther(tx.value));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
this._log('sign_transaction', 'SIGNED', { tx: this._sanitizeTx(tx) });
|
|
181
|
+
return signedTx;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Send a transaction (sign + broadcast)
|
|
186
|
+
*/
|
|
187
|
+
async sendTransaction(tx) {
|
|
188
|
+
const errors = this.validateTransaction(tx);
|
|
189
|
+
if (errors.length > 0) {
|
|
190
|
+
this._log('send_transaction', 'DENIED', { errors, tx: this._sanitizeTx(tx) });
|
|
191
|
+
throw new Error(`Transaction blocked: ${errors.join('; ')}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const response = await this.signer.sendTransaction(tx);
|
|
195
|
+
const receipt = await response.wait();
|
|
196
|
+
|
|
197
|
+
if (tx.value) {
|
|
198
|
+
this.dailySpent += parseFloat(ethers.formatEther(tx.value));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
this._log('send_transaction', 'SENT', {
|
|
202
|
+
hash: receipt.hash,
|
|
203
|
+
block: receipt.blockNumber,
|
|
204
|
+
gasUsed: receipt.gasUsed.toString(),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
hash: receipt.hash,
|
|
209
|
+
blockNumber: receipt.blockNumber,
|
|
210
|
+
gasUsed: receipt.gasUsed.toString(),
|
|
211
|
+
status: receipt.status,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Sign a message (EIP-191)
|
|
217
|
+
*/
|
|
218
|
+
async signMessage(message) {
|
|
219
|
+
const sig = await this.signer.signMessage(message);
|
|
220
|
+
this._log('sign_message', 'SIGNED', { messagePreview: message.slice(0, 100) });
|
|
221
|
+
return sig;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Sign typed data (EIP-712)
|
|
226
|
+
*/
|
|
227
|
+
async signTypedData(domain, types, value) {
|
|
228
|
+
const sig = await this.signer.signTypedData(domain, types, value);
|
|
229
|
+
this._log('sign_typed_data', 'SIGNED', { domain: domain.name });
|
|
230
|
+
return sig;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Start the HTTP signing proxy
|
|
235
|
+
*/
|
|
236
|
+
async startServer() {
|
|
237
|
+
return new Promise((resolve, reject) => {
|
|
238
|
+
this.server = createServer(async (req, res) => {
|
|
239
|
+
// CORS headers
|
|
240
|
+
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:*');
|
|
241
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
|
|
242
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
243
|
+
|
|
244
|
+
if (req.method === 'OPTIONS') {
|
|
245
|
+
res.writeHead(204);
|
|
246
|
+
res.end();
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Auth check
|
|
251
|
+
const auth = req.headers.authorization;
|
|
252
|
+
if (!auth || auth !== `Bearer ${this.bearerToken}`) {
|
|
253
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
254
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
255
|
+
this._log('auth', 'DENIED', { ip: req.socket.remoteAddress });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const body = await this._readBody(req);
|
|
261
|
+
const result = await this._handleRequest(req.url, body);
|
|
262
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
263
|
+
res.end(JSON.stringify(result));
|
|
264
|
+
} catch (err) {
|
|
265
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
266
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
this.server.listen(this.port, this.host, () => {
|
|
271
|
+
resolve();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
this.server.on('error', reject);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Handle API request routing
|
|
280
|
+
*/
|
|
281
|
+
async _handleRequest(url, body) {
|
|
282
|
+
switch (url) {
|
|
283
|
+
case '/address':
|
|
284
|
+
return { address: this.address, chain: this.chain };
|
|
285
|
+
|
|
286
|
+
case '/balance': {
|
|
287
|
+
const balance = await this.provider.getBalance(this.address);
|
|
288
|
+
return {
|
|
289
|
+
address: this.address,
|
|
290
|
+
balance: ethers.formatEther(balance),
|
|
291
|
+
chain: this.chain,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
case '/chain':
|
|
296
|
+
return { chain: this.chain, rpc: getRPC(this.chain) };
|
|
297
|
+
|
|
298
|
+
case '/sign': {
|
|
299
|
+
const signed = await this.signTransaction(body);
|
|
300
|
+
return { signed };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
case '/send': {
|
|
304
|
+
const result = await this.sendTransaction(body);
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
case '/sign-message': {
|
|
309
|
+
const sig = await this.signMessage(body.message);
|
|
310
|
+
return { signature: sig, address: this.address };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
case '/sign-typed-data': {
|
|
314
|
+
const sig = await this.signTypedData(body.domain, body.types, body.value);
|
|
315
|
+
return { signature: sig, address: this.address };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
case '/policy':
|
|
319
|
+
return {
|
|
320
|
+
maxValuePerTx: this.policy.maxValuePerTx,
|
|
321
|
+
dailySpendLimit: this.policy.dailySpendLimit,
|
|
322
|
+
dailySpent: this.dailySpent,
|
|
323
|
+
dailyRemaining: this.policy.dailySpendLimit - this.dailySpent,
|
|
324
|
+
allowlistedContracts: this.policy.allowlistedContracts.length || 'all',
|
|
325
|
+
confirmAbove: this.policy.confirmAbove,
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
case '/audit':
|
|
329
|
+
return { log: this.auditLog.slice(-50) };
|
|
330
|
+
|
|
331
|
+
case '/health':
|
|
332
|
+
return {
|
|
333
|
+
status: 'ok',
|
|
334
|
+
address: this.address,
|
|
335
|
+
chain: this.chain,
|
|
336
|
+
uptime: Math.floor((Date.now() - this.dailyResetTime) / 1000),
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
// SECURITY: No endpoint for /private-key, /key, /export, /mnemonic, /seed
|
|
340
|
+
// These are intentionally absent. The PK lives only in this.signer.
|
|
341
|
+
|
|
342
|
+
default:
|
|
343
|
+
throw new Error(`Unknown endpoint: ${url}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Stop the server
|
|
349
|
+
*/
|
|
350
|
+
stop() {
|
|
351
|
+
if (this.server) {
|
|
352
|
+
this.server.close();
|
|
353
|
+
this.server = null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Internal helpers
|
|
358
|
+
|
|
359
|
+
_readBody(req) {
|
|
360
|
+
return new Promise((resolve) => {
|
|
361
|
+
if (req.method === 'GET') return resolve({});
|
|
362
|
+
let data = '';
|
|
363
|
+
req.on('data', chunk => data += chunk);
|
|
364
|
+
req.on('end', () => {
|
|
365
|
+
try { resolve(JSON.parse(data)); }
|
|
366
|
+
catch { resolve({}); }
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
_sanitizeTx(tx) {
|
|
372
|
+
return {
|
|
373
|
+
to: tx.to,
|
|
374
|
+
value: tx.value ? ethers.formatEther(tx.value) + ' ETH' : '0',
|
|
375
|
+
dataLength: tx.data ? tx.data.length : 0,
|
|
376
|
+
selector: tx.data?.slice(0, 10) || null,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
_log(operation, status, details = {}) {
|
|
381
|
+
if (!this.policy.auditLog) return;
|
|
382
|
+
this.auditLog.push({
|
|
383
|
+
timestamp: new Date().toISOString(),
|
|
384
|
+
operation,
|
|
385
|
+
status,
|
|
386
|
+
...details,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ══════════════════════════════════════════════════
|
|
392
|
+
// CLI COMMANDS
|
|
393
|
+
// ══════════════════════════════════════════════════
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Start the agent signer service
|
|
397
|
+
*/
|
|
398
|
+
export async function startAgentSigner(walletName, opts = {}) {
|
|
399
|
+
walletName = walletName || getConfig('activeWallet');
|
|
400
|
+
if (!walletName) {
|
|
401
|
+
error('No wallet specified. Use: darksol agent start <wallet-name>');
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const { password } = await inquirer.prompt([{
|
|
406
|
+
type: 'password',
|
|
407
|
+
name: 'password',
|
|
408
|
+
message: theme.gold('Wallet password:'),
|
|
409
|
+
mask: '●',
|
|
410
|
+
}]);
|
|
411
|
+
|
|
412
|
+
const spin = spinner('Starting agent signer...').start();
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
const signer = new AgentSigner(walletName, password, {
|
|
416
|
+
port: opts.port || 18790,
|
|
417
|
+
policy: {
|
|
418
|
+
maxValuePerTx: parseFloat(opts.maxValue || '1.0'),
|
|
419
|
+
dailySpendLimit: parseFloat(opts.dailyLimit || '5.0'),
|
|
420
|
+
allowlistedContracts: opts.allowlist ? opts.allowlist.split(',') : [],
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
await signer.init();
|
|
425
|
+
await signer.startServer();
|
|
426
|
+
|
|
427
|
+
spin.succeed('Agent signer running');
|
|
428
|
+
|
|
429
|
+
console.log('');
|
|
430
|
+
showSection('🔐 DARKSOL AGENT SIGNER');
|
|
431
|
+
kvDisplay([
|
|
432
|
+
['Status', theme.success('● ACTIVE')],
|
|
433
|
+
['Wallet', walletName],
|
|
434
|
+
['Address', signer.address],
|
|
435
|
+
['Chain', signer.chain],
|
|
436
|
+
['Endpoint', `http://${signer.host}:${signer.port}`],
|
|
437
|
+
['Max/TX', `${signer.policy.maxValuePerTx} ETH`],
|
|
438
|
+
['Daily Limit', `${signer.policy.dailySpendLimit} ETH`],
|
|
439
|
+
['Contracts', signer.policy.allowlistedContracts.length ? signer.policy.allowlistedContracts.length + ' allowlisted' : 'All allowed'],
|
|
440
|
+
]);
|
|
441
|
+
|
|
442
|
+
console.log('');
|
|
443
|
+
showSection('BEARER TOKEN (show once — copy now)');
|
|
444
|
+
console.log(theme.accent(` ${signer.bearerToken}`));
|
|
445
|
+
|
|
446
|
+
console.log('');
|
|
447
|
+
showSection('OPENCLAW INTEGRATION');
|
|
448
|
+
console.log(theme.dim(' Add to your OpenClaw config:'));
|
|
449
|
+
console.log('');
|
|
450
|
+
console.log(theme.gold(' tools:'));
|
|
451
|
+
console.log(theme.dim(' darksol-signer:'));
|
|
452
|
+
console.log(theme.dim(` url: http://127.0.0.1:${signer.port}`));
|
|
453
|
+
console.log(theme.dim(` token: ${signer.bearerToken}`));
|
|
454
|
+
console.log('');
|
|
455
|
+
|
|
456
|
+
showSection('API ENDPOINTS');
|
|
457
|
+
const endpoints = [
|
|
458
|
+
['GET /address', 'Get wallet address (safe)'],
|
|
459
|
+
['GET /balance', 'Get ETH balance (safe)'],
|
|
460
|
+
['GET /chain', 'Get active chain (safe)'],
|
|
461
|
+
['POST /send', 'Sign + broadcast transaction'],
|
|
462
|
+
['POST /sign', 'Sign transaction (return raw)'],
|
|
463
|
+
['POST /sign-message', 'Sign EIP-191 message'],
|
|
464
|
+
['POST /sign-typed-data', 'Sign EIP-712 typed data'],
|
|
465
|
+
['GET /policy', 'View spending policy'],
|
|
466
|
+
['GET /audit', 'View audit log'],
|
|
467
|
+
['GET /health', 'Health check'],
|
|
468
|
+
];
|
|
469
|
+
endpoints.forEach(([ep, desc]) => {
|
|
470
|
+
console.log(` ${theme.gold(ep.padEnd(26))} ${theme.dim(desc)}`);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
console.log('');
|
|
474
|
+
showSection('SECURITY');
|
|
475
|
+
console.log(theme.dim(' ✓ Private key NEVER exposed via any endpoint'));
|
|
476
|
+
console.log(theme.dim(' ✓ Loopback-only (127.0.0.1) — not accessible from network'));
|
|
477
|
+
console.log(theme.dim(' ✓ Bearer token auth required for every request'));
|
|
478
|
+
console.log(theme.dim(' ✓ Per-TX value limits + daily spending cap'));
|
|
479
|
+
console.log(theme.dim(' ✓ Contract allowlist support'));
|
|
480
|
+
console.log(theme.dim(' ✓ Dangerous function selectors blocked'));
|
|
481
|
+
console.log(theme.dim(' ✓ Full audit log of all operations'));
|
|
482
|
+
console.log(theme.dim(' ✓ Prompt injection resistant — LLM never sees the key'));
|
|
483
|
+
console.log('');
|
|
484
|
+
|
|
485
|
+
warn('Press Ctrl+C to stop the signer');
|
|
486
|
+
info('The signer runs until you stop it. Your key stays in memory only.');
|
|
487
|
+
|
|
488
|
+
// Keep alive
|
|
489
|
+
await new Promise(() => {});
|
|
490
|
+
|
|
491
|
+
} catch (err) {
|
|
492
|
+
spin.fail('Failed to start agent signer');
|
|
493
|
+
error(err.message);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Show agent signer documentation
|
|
499
|
+
*/
|
|
500
|
+
export function showAgentDocs() {
|
|
501
|
+
showSection('🔐 DARKSOL AGENT SIGNER — SECURITY MODEL');
|
|
502
|
+
console.log('');
|
|
503
|
+
|
|
504
|
+
console.log(theme.gold(' THE PROBLEM'));
|
|
505
|
+
console.log(theme.dim(' AI agents need to sign transactions, but exposing private'));
|
|
506
|
+
console.log(theme.dim(' keys to LLMs is dangerous. Prompt injection attacks could'));
|
|
507
|
+
console.log(theme.dim(' trick the AI into revealing the key. Existing agent wallets'));
|
|
508
|
+
console.log(theme.dim(' (Bankr, Phantom MCP) can\'t execute x402 payments or sign'));
|
|
509
|
+
console.log(theme.dim(' arbitrary contracts in the wild.'));
|
|
510
|
+
console.log('');
|
|
511
|
+
|
|
512
|
+
console.log(theme.gold(' THE SOLUTION'));
|
|
513
|
+
console.log(theme.dim(' DARKSOL Agent Signer is a local signing proxy:'));
|
|
514
|
+
console.log(theme.dim(' 1. You unlock your wallet ONCE with your password'));
|
|
515
|
+
console.log(theme.dim(' 2. The key decrypts into memory (never to disk/API)'));
|
|
516
|
+
console.log(theme.dim(' 3. A local HTTP server exposes signing endpoints'));
|
|
517
|
+
console.log(theme.dim(' 4. AI agents call /send, /sign — never see the key'));
|
|
518
|
+
console.log(theme.dim(' 5. Every TX is validated against your security policy'));
|
|
519
|
+
console.log('');
|
|
520
|
+
|
|
521
|
+
console.log(theme.gold(' WHY IT\'S SAFE'));
|
|
522
|
+
console.log(theme.dim(' ✓ No /private-key endpoint exists — literally no way to extract it'));
|
|
523
|
+
console.log(theme.dim(' ✓ Loopback-only — only your machine can reach it'));
|
|
524
|
+
console.log(theme.dim(' ✓ Bearer token — one-time auth shown only in your terminal'));
|
|
525
|
+
console.log(theme.dim(' ✓ Spending limits — cap per TX and per day'));
|
|
526
|
+
console.log(theme.dim(' ✓ Contract allowlist — restrict which contracts can be called'));
|
|
527
|
+
console.log(theme.dim(' ✓ Blocked selectors — transferOwnership, selfdestruct blocked'));
|
|
528
|
+
console.log(theme.dim(' ✓ Audit log — every operation is recorded'));
|
|
529
|
+
console.log(theme.dim(' ✓ Prompt injection proof — the LLM literally cannot access the key'));
|
|
530
|
+
console.log(theme.dim(' because it doesn\'t exist in any API response'));
|
|
531
|
+
console.log('');
|
|
532
|
+
|
|
533
|
+
console.log(theme.gold(' HOW TO USE WITH OPENCLAW'));
|
|
534
|
+
console.log(theme.dim(' 1. Create a wallet: darksol wallet create agent-wallet'));
|
|
535
|
+
console.log(theme.dim(' 2. Start the signer: darksol agent start agent-wallet'));
|
|
536
|
+
console.log(theme.dim(' 3. Copy the bearer token shown in terminal'));
|
|
537
|
+
console.log(theme.dim(' 4. Add to OpenClaw config or agent\'s TOOLS.md'));
|
|
538
|
+
console.log(theme.dim(' 5. Agent calls http://127.0.0.1:18790/send to sign TXs'));
|
|
539
|
+
console.log(theme.dim(' 6. Agent calls http://127.0.0.1:18790/sign-message for x402'));
|
|
540
|
+
console.log('');
|
|
541
|
+
|
|
542
|
+
console.log(theme.gold(' x402 PAYMENT SIGNING'));
|
|
543
|
+
console.log(theme.dim(' The agent can sign x402 payment authorizations via'));
|
|
544
|
+
console.log(theme.dim(' /sign-typed-data (EIP-712). This enables real x402'));
|
|
545
|
+
console.log(theme.dim(' payments in the wild — something Bankr and Phantom'));
|
|
546
|
+
console.log(theme.dim(' MCP wallets cannot do. Your agent gets a real wallet'));
|
|
547
|
+
console.log(theme.dim(' that can pay for services autonomously.'));
|
|
548
|
+
console.log('');
|
|
549
|
+
|
|
550
|
+
console.log(theme.gold(' SPENDING POLICY'));
|
|
551
|
+
console.log(theme.dim(' --max-value 1.0 Max ETH per transaction (default: 1.0)'));
|
|
552
|
+
console.log(theme.dim(' --daily-limit 5.0 Max ETH per day (default: 5.0)'));
|
|
553
|
+
console.log(theme.dim(' --allowlist 0x.. Only allow these contracts (comma-sep)'));
|
|
554
|
+
console.log(theme.dim(' --port 18790 Signer port (default: 18790)'));
|
|
555
|
+
console.log('');
|
|
556
|
+
}
|