@exagent/agent 0.2.1 → 0.3.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/.turbo/turbo-build.log +6 -6
- package/dist/chunk-4UVMO6ZM.js +6318 -0
- package/dist/chunk-5NU6FDDE.js +6020 -0
- package/dist/chunk-GPMXUMYH.js +5991 -0
- package/dist/chunk-IJK4EFTJ.js +6043 -0
- package/dist/chunk-J3NG7AGT.js +6047 -0
- package/dist/chunk-QG22GADV.js +6316 -0
- package/dist/chunk-SVFTC5V2.js +6021 -0
- package/dist/chunk-TDACLKD7.js +5867 -0
- package/dist/chunk-UAP5CTHB.js +5985 -0
- package/dist/chunk-VDK4XPAC.js +6318 -0
- package/dist/cli.js +337 -15
- package/dist/index.d.ts +29 -1
- package/dist/index.js +1 -1
- package/package.json +15 -8
- package/src/cli.ts +54 -15
- package/src/config.ts +231 -25
- package/src/runtime.ts +370 -37
- package/src/scrub-secrets.ts +39 -0
- package/src/setup.ts +332 -0
- package/src/strategy/loader.ts +132 -1
- package/src/ui.ts +75 -0
package/src/config.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, resolve } from 'node:path';
|
|
2
5
|
import { z } from 'zod';
|
|
3
6
|
import type { LLMProvider } from '@exagent/sdk';
|
|
4
7
|
|
|
@@ -19,7 +22,14 @@ export interface RuntimeConfig {
|
|
|
19
22
|
};
|
|
20
23
|
strategy: {
|
|
21
24
|
file?: string;
|
|
25
|
+
code?: string;
|
|
22
26
|
template?: string;
|
|
27
|
+
venues?: string[];
|
|
28
|
+
prompt?: {
|
|
29
|
+
name?: string;
|
|
30
|
+
systemPrompt: string;
|
|
31
|
+
venues?: string[];
|
|
32
|
+
};
|
|
23
33
|
};
|
|
24
34
|
trading: {
|
|
25
35
|
mode: 'live' | 'paper';
|
|
@@ -79,15 +89,43 @@ export interface RuntimeConfig {
|
|
|
79
89
|
};
|
|
80
90
|
}
|
|
81
91
|
|
|
82
|
-
|
|
92
|
+
export interface LocalSecretPayload {
|
|
93
|
+
apiToken: string;
|
|
94
|
+
walletPrivateKey?: string;
|
|
95
|
+
llmApiKey?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface SecureStoreFile {
|
|
99
|
+
version: 1;
|
|
100
|
+
algorithm: 'aes-256-gcm';
|
|
101
|
+
kdf: {
|
|
102
|
+
name: 'scrypt';
|
|
103
|
+
salt: string;
|
|
104
|
+
keyLength: 32;
|
|
105
|
+
cost: number;
|
|
106
|
+
blockSize: number;
|
|
107
|
+
parallelization: number;
|
|
108
|
+
};
|
|
109
|
+
iv: string;
|
|
110
|
+
ciphertext: string;
|
|
111
|
+
authTag: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface LoadConfigOptions {
|
|
115
|
+
getSecretPassword?: () => Promise<string>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const providerEnum = z.enum(['openai', 'anthropic', 'google', 'deepseek', 'mistral', 'groq', 'together', 'ollama']);
|
|
119
|
+
|
|
120
|
+
const runtimeSchema = z.object({
|
|
83
121
|
agentId: z.string(),
|
|
84
122
|
apiUrl: z.string().url(),
|
|
85
|
-
apiToken: z.string(),
|
|
123
|
+
apiToken: z.string().min(1),
|
|
86
124
|
wallet: z.object({
|
|
87
|
-
privateKey: z.string(),
|
|
125
|
+
privateKey: z.string().regex(/^0x[a-fA-F0-9]{64}$/),
|
|
88
126
|
}).optional(),
|
|
89
127
|
llm: z.object({
|
|
90
|
-
provider:
|
|
128
|
+
provider: providerEnum,
|
|
91
129
|
model: z.string().optional(),
|
|
92
130
|
apiKey: z.string().optional(),
|
|
93
131
|
endpoint: z.string().optional(),
|
|
@@ -96,7 +134,14 @@ const configSchema = z.object({
|
|
|
96
134
|
}),
|
|
97
135
|
strategy: z.object({
|
|
98
136
|
file: z.string().optional(),
|
|
137
|
+
code: z.string().optional(),
|
|
99
138
|
template: z.string().optional(),
|
|
139
|
+
venues: z.array(z.string()).optional(),
|
|
140
|
+
prompt: z.object({
|
|
141
|
+
name: z.string().optional(),
|
|
142
|
+
systemPrompt: z.string().min(1),
|
|
143
|
+
venues: z.array(z.string()).optional(),
|
|
144
|
+
}).optional(),
|
|
100
145
|
}),
|
|
101
146
|
trading: z.object({
|
|
102
147
|
mode: z.enum(['live', 'paper']).default('paper'),
|
|
@@ -156,23 +201,174 @@ const configSchema = z.object({
|
|
|
156
201
|
}).optional(),
|
|
157
202
|
});
|
|
158
203
|
|
|
159
|
-
|
|
204
|
+
const configFileSchema = z.object({
|
|
205
|
+
agentId: z.string(),
|
|
206
|
+
apiUrl: z.string().url(),
|
|
207
|
+
apiToken: z.string().min(1).optional(),
|
|
208
|
+
wallet: z.object({
|
|
209
|
+
privateKey: z.string().regex(/^0x[a-fA-F0-9]{64}$/),
|
|
210
|
+
}).optional(),
|
|
211
|
+
llm: z.object({
|
|
212
|
+
provider: providerEnum.optional(),
|
|
213
|
+
model: z.string().optional(),
|
|
214
|
+
apiKey: z.string().optional(),
|
|
215
|
+
endpoint: z.string().optional(),
|
|
216
|
+
temperature: z.number().min(0).max(2).optional(),
|
|
217
|
+
maxTokens: z.number().optional(),
|
|
218
|
+
}).default({}),
|
|
219
|
+
strategy: z.object({
|
|
220
|
+
file: z.string().optional(),
|
|
221
|
+
code: z.string().optional(),
|
|
222
|
+
template: z.string().optional(),
|
|
223
|
+
venues: z.array(z.string()).optional(),
|
|
224
|
+
prompt: z.object({
|
|
225
|
+
name: z.string().optional(),
|
|
226
|
+
systemPrompt: z.string().min(1),
|
|
227
|
+
venues: z.array(z.string()).optional(),
|
|
228
|
+
}).optional(),
|
|
229
|
+
}),
|
|
230
|
+
trading: runtimeSchema.shape.trading,
|
|
231
|
+
venues: runtimeSchema.shape.venues,
|
|
232
|
+
relay: runtimeSchema.shape.relay,
|
|
233
|
+
llmBudget: runtimeSchema.shape.llmBudget,
|
|
234
|
+
rpcOverrides: runtimeSchema.shape.rpcOverrides,
|
|
235
|
+
logging: runtimeSchema.shape.logging,
|
|
236
|
+
secrets: z.object({
|
|
237
|
+
bootstrapToken: z.string().optional(),
|
|
238
|
+
bootstrapExpiresAt: z.string().optional(),
|
|
239
|
+
secureStorePath: z.string().optional(),
|
|
240
|
+
}).optional(),
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const secureStoreSchema = z.object({
|
|
244
|
+
version: z.literal(1),
|
|
245
|
+
algorithm: z.literal('aes-256-gcm'),
|
|
246
|
+
kdf: z.object({
|
|
247
|
+
name: z.literal('scrypt'),
|
|
248
|
+
salt: z.string(),
|
|
249
|
+
keyLength: z.literal(32),
|
|
250
|
+
cost: z.number().int().positive(),
|
|
251
|
+
blockSize: z.number().int().positive(),
|
|
252
|
+
parallelization: z.number().int().positive(),
|
|
253
|
+
}),
|
|
254
|
+
iv: z.string(),
|
|
255
|
+
ciphertext: z.string(),
|
|
256
|
+
authTag: z.string(),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
export type RuntimeConfigFile = z.infer<typeof configFileSchema>;
|
|
260
|
+
|
|
261
|
+
const DEFAULT_SCRYPT_COST = 32768;
|
|
262
|
+
const DEFAULT_SCRYPT_BLOCK_SIZE = 8;
|
|
263
|
+
const DEFAULT_SCRYPT_PARALLELIZATION = 1;
|
|
264
|
+
|
|
265
|
+
function expandHomeDir(path: string): string {
|
|
266
|
+
if (!path.startsWith('~/')) return path;
|
|
267
|
+
return resolve(homedir(), path.slice(2));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function getDefaultSecureStorePath(agentId: string): string {
|
|
271
|
+
return resolve(homedir(), '.exagent', 'agents', agentId, 'secrets.json');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function readConfigFile(path: string = 'agent-config.json'): RuntimeConfigFile {
|
|
160
275
|
if (!existsSync(path)) {
|
|
161
276
|
throw new Error(`Config file not found: ${path}. Run 'exagent init' first.`);
|
|
162
277
|
}
|
|
163
278
|
|
|
164
|
-
const raw = readFileSync(path, 'utf-8');
|
|
165
279
|
let parsed: unknown;
|
|
166
|
-
|
|
167
280
|
try {
|
|
168
|
-
parsed = JSON.parse(
|
|
281
|
+
parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
169
282
|
} catch {
|
|
170
283
|
throw new Error(`Invalid JSON in ${path}`);
|
|
171
284
|
}
|
|
172
285
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
286
|
+
const result = configFileSchema.safeParse(parsed);
|
|
287
|
+
if (!result.success) {
|
|
288
|
+
const issues = result.error.issues.map((issue) => ` ${issue.path.join('.')}: ${issue.message}`).join('\n');
|
|
289
|
+
throw new Error(`Invalid config file:\n${issues}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return result.data;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function writeConfigFile(path: string, config: RuntimeConfigFile): void {
|
|
296
|
+
writeFileSync(path, JSON.stringify(config, null, 2));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function encryptSecretPayload(payload: LocalSecretPayload, password: string): SecureStoreFile {
|
|
300
|
+
const salt = randomBytes(16);
|
|
301
|
+
const iv = randomBytes(12);
|
|
302
|
+
const key = scryptSync(password, salt, 32, {
|
|
303
|
+
N: DEFAULT_SCRYPT_COST,
|
|
304
|
+
r: DEFAULT_SCRYPT_BLOCK_SIZE,
|
|
305
|
+
p: DEFAULT_SCRYPT_PARALLELIZATION,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
|
309
|
+
const plaintext = Buffer.from(JSON.stringify(payload), 'utf8');
|
|
310
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
version: 1,
|
|
314
|
+
algorithm: 'aes-256-gcm',
|
|
315
|
+
kdf: {
|
|
316
|
+
name: 'scrypt',
|
|
317
|
+
salt: salt.toString('hex'),
|
|
318
|
+
keyLength: 32,
|
|
319
|
+
cost: DEFAULT_SCRYPT_COST,
|
|
320
|
+
blockSize: DEFAULT_SCRYPT_BLOCK_SIZE,
|
|
321
|
+
parallelization: DEFAULT_SCRYPT_PARALLELIZATION,
|
|
322
|
+
},
|
|
323
|
+
iv: iv.toString('hex'),
|
|
324
|
+
ciphertext: ciphertext.toString('hex'),
|
|
325
|
+
authTag: cipher.getAuthTag().toString('hex'),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function decryptSecretPayload(path: string, password: string): LocalSecretPayload {
|
|
330
|
+
const secureStorePath = expandHomeDir(path);
|
|
331
|
+
if (!existsSync(secureStorePath)) {
|
|
332
|
+
throw new Error(`Encrypted secret store not found: ${secureStorePath}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
let parsed: unknown;
|
|
336
|
+
try {
|
|
337
|
+
parsed = JSON.parse(readFileSync(secureStorePath, 'utf-8'));
|
|
338
|
+
} catch {
|
|
339
|
+
throw new Error(`Invalid JSON in encrypted secret store: ${secureStorePath}`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const result = secureStoreSchema.safeParse(parsed);
|
|
343
|
+
if (!result.success) {
|
|
344
|
+
const issues = result.error.issues.map((issue) => ` ${issue.path.join('.')}: ${issue.message}`).join('\n');
|
|
345
|
+
throw new Error(`Invalid encrypted secret store:\n${issues}`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const store = result.data;
|
|
349
|
+
const key = scryptSync(password, Buffer.from(store.kdf.salt, 'hex'), store.kdf.keyLength, {
|
|
350
|
+
N: store.kdf.cost,
|
|
351
|
+
r: store.kdf.blockSize,
|
|
352
|
+
p: store.kdf.parallelization,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
const decipher = createDecipheriv(store.algorithm, key, Buffer.from(store.iv, 'hex'));
|
|
357
|
+
decipher.setAuthTag(Buffer.from(store.authTag, 'hex'));
|
|
358
|
+
const plaintext = Buffer.concat([
|
|
359
|
+
decipher.update(Buffer.from(store.ciphertext, 'hex')),
|
|
360
|
+
decipher.final(),
|
|
361
|
+
]);
|
|
362
|
+
return JSON.parse(plaintext.toString('utf8')) as LocalSecretPayload;
|
|
363
|
+
} catch {
|
|
364
|
+
throw new Error('Unable to decrypt local secret store. Check your password.');
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export async function loadConfig(path: string = 'agent-config.json', options: LoadConfigOptions = {}): Promise<RuntimeConfig> {
|
|
369
|
+
const parsed = readConfigFile(path);
|
|
370
|
+
const config = structuredClone(parsed) as RuntimeConfigFile & Record<string, unknown>;
|
|
371
|
+
const llm = { ...(config.llm || {}) } as Record<string, unknown>;
|
|
176
372
|
|
|
177
373
|
if (process.env.EXAGENT_LLM_PROVIDER) llm.provider = process.env.EXAGENT_LLM_PROVIDER;
|
|
178
374
|
if (process.env.EXAGENT_LLM_MODEL) llm.model = process.env.EXAGENT_LLM_MODEL;
|
|
@@ -183,9 +379,24 @@ export function loadConfig(path: string = 'agent-config.json'): RuntimeConfig {
|
|
|
183
379
|
config.wallet = { privateKey: process.env.EXAGENT_WALLET_PRIVATE_KEY };
|
|
184
380
|
}
|
|
185
381
|
|
|
382
|
+
if ((!config.apiToken || !llm.apiKey || !config.wallet) && parsed.secrets?.secureStorePath) {
|
|
383
|
+
const password = process.env.EXAGENT_SECRET_PASSWORD || await options.getSecretPassword?.();
|
|
384
|
+
if (!password) {
|
|
385
|
+
throw new Error('Encrypted secret store found, but no password was provided.');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const secrets = decryptSecretPayload(parsed.secrets.secureStorePath, password);
|
|
389
|
+
if (!config.apiToken) config.apiToken = secrets.apiToken;
|
|
390
|
+
if (!config.wallet && secrets.walletPrivateKey) config.wallet = { privateKey: secrets.walletPrivateKey };
|
|
391
|
+
if (!llm.apiKey && secrets.llmApiKey) llm.apiKey = secrets.llmApiKey;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if ((!config.apiToken || !llm.apiKey || !config.wallet) && parsed.secrets?.bootstrapToken && !parsed.secrets?.secureStorePath) {
|
|
395
|
+
throw new Error(`Config ${path} still requires first-time secure setup. Run 'exagent setup --config ${path}' or start the agent interactively.`);
|
|
396
|
+
}
|
|
397
|
+
|
|
186
398
|
config.llm = llm;
|
|
187
399
|
|
|
188
|
-
// Apply RPC endpoint overrides from environment
|
|
189
400
|
const rpcOverrides: Record<string, string> = (config.rpcOverrides as Record<string, string>) || {};
|
|
190
401
|
if (process.env.EXAGENT_RPC_BASE) rpcOverrides.base = process.env.EXAGENT_RPC_BASE;
|
|
191
402
|
if (process.env.EXAGENT_RPC_ARBITRUM) rpcOverrides.arbitrum = process.env.EXAGENT_RPC_ARBITRUM;
|
|
@@ -195,9 +406,9 @@ export function loadConfig(path: string = 'agent-config.json'): RuntimeConfig {
|
|
|
195
406
|
config.rpcOverrides = rpcOverrides;
|
|
196
407
|
}
|
|
197
408
|
|
|
198
|
-
const result =
|
|
409
|
+
const result = runtimeSchema.safeParse(config);
|
|
199
410
|
if (!result.success) {
|
|
200
|
-
const issues = result.error.issues.map(
|
|
411
|
+
const issues = result.error.issues.map((issue) => ` ${issue.path.join('.')}: ${issue.message}`).join('\n');
|
|
201
412
|
throw new Error(`Invalid config:\n${issues}`);
|
|
202
413
|
}
|
|
203
414
|
|
|
@@ -205,18 +416,10 @@ export function loadConfig(path: string = 'agent-config.json'): RuntimeConfig {
|
|
|
205
416
|
}
|
|
206
417
|
|
|
207
418
|
export function generateSampleConfig(agentId: string, apiUrl: string): string {
|
|
208
|
-
const config = {
|
|
419
|
+
const config: RuntimeConfigFile = {
|
|
209
420
|
agentId,
|
|
210
421
|
apiUrl,
|
|
211
|
-
|
|
212
|
-
wallet: {
|
|
213
|
-
privateKey: '<your-hex-private-key>',
|
|
214
|
-
},
|
|
215
|
-
llm: {
|
|
216
|
-
provider: 'openai',
|
|
217
|
-
model: 'gpt-4o',
|
|
218
|
-
apiKey: '<your-api-key>',
|
|
219
|
-
},
|
|
422
|
+
llm: {},
|
|
220
423
|
strategy: {
|
|
221
424
|
template: 'momentum',
|
|
222
425
|
},
|
|
@@ -266,6 +469,9 @@ export function generateSampleConfig(agentId: string, apiUrl: string): string {
|
|
|
266
469
|
heartbeatIntervalMs: 30000,
|
|
267
470
|
reconnectMaxAttempts: 50,
|
|
268
471
|
},
|
|
472
|
+
secrets: {
|
|
473
|
+
secureStorePath: getDefaultSecureStorePath(agentId),
|
|
474
|
+
},
|
|
269
475
|
};
|
|
270
476
|
|
|
271
477
|
return JSON.stringify(config, null, 2);
|