@exagent/agent 0.2.0 → 0.3.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/dist/cli.js CHANGED
@@ -1,24 +1,220 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  AgentRuntime,
4
+ encryptSecretPayload,
5
+ getDefaultSecureStorePath,
4
6
  listTemplates,
5
7
  loadConfig,
8
+ readConfigFile,
9
+ writeConfigFile,
6
10
  writeSampleConfig
7
- } from "./chunk-B4VHIITU.js";
11
+ } from "./chunk-UAP5CTHB.js";
8
12
 
9
13
  // src/cli.ts
10
14
  import { Command } from "commander";
15
+
16
+ // src/setup.ts
17
+ import { chmodSync, existsSync, mkdirSync, writeFileSync } from "fs";
18
+ import { homedir } from "os";
19
+ import { dirname, resolve } from "path";
20
+ import { createInterface } from "readline/promises";
21
+ import { stdin as input, stdout as output } from "process";
22
+ import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
23
+ function expandHomeDir(path) {
24
+ if (!path.startsWith("~/")) return path;
25
+ return resolve(homedir(), path.slice(2));
26
+ }
27
+ async function prompt(question) {
28
+ const rl = createInterface({ input, output });
29
+ try {
30
+ return (await rl.question(question)).trim();
31
+ } finally {
32
+ rl.close();
33
+ }
34
+ }
35
+ async function promptSecret(question) {
36
+ const rl = createInterface({ input, output, terminal: true });
37
+ rl.stdoutMuted = true;
38
+ const originalWrite = rl._writeToOutput?.bind(rl);
39
+ rl._writeToOutput = (text) => {
40
+ if (!rl.stdoutMuted) {
41
+ originalWrite?.(text);
42
+ }
43
+ };
44
+ try {
45
+ const answer = (await rl.question(question)).trim();
46
+ output.write("\n");
47
+ return answer;
48
+ } finally {
49
+ rl.close();
50
+ }
51
+ }
52
+ async function promptSecretPassword(question = "Device password: ") {
53
+ return promptSecret(question);
54
+ }
55
+ async function promptPasswordWithConfirmation() {
56
+ output.write("\n");
57
+ output.write("Important: this password encrypts the local wallet key, relay token, and agent LLM key for this device.\n");
58
+ output.write("If you lose this password and the agent wallet holds funds, those funds cannot be recovered.\n\n");
59
+ const ack = await prompt('Type "I UNDERSTAND" to continue: ');
60
+ if (ack !== "I UNDERSTAND") {
61
+ throw new Error("Secure setup aborted");
62
+ }
63
+ while (true) {
64
+ const password = await promptSecret("Create a device password (min 12 chars): ");
65
+ if (password.length < 12) {
66
+ output.write("Password must be at least 12 characters.\n");
67
+ continue;
68
+ }
69
+ const confirm = await promptSecret("Confirm device password: ");
70
+ if (password !== confirm) {
71
+ output.write("Passwords did not match. Try again.\n");
72
+ continue;
73
+ }
74
+ return password;
75
+ }
76
+ }
77
+ async function promptWalletPrivateKey() {
78
+ while (true) {
79
+ const choice = (await prompt("Wallet setup \u2014 [1] generate new wallet locally, [2] use existing private key: ")).trim();
80
+ if (choice === "1") {
81
+ const privateKey = generatePrivateKey();
82
+ const address = privateKeyToAccount(privateKey).address;
83
+ output.write(`Generated wallet address: ${address}
84
+ `);
85
+ return privateKey;
86
+ }
87
+ if (choice === "2") {
88
+ const privateKey = await promptSecret("Wallet private key (0x...): ");
89
+ if (/^0x[a-fA-F0-9]{64}$/.test(privateKey)) {
90
+ return privateKey;
91
+ }
92
+ output.write("Invalid private key. Expected a 32-byte hex string prefixed with 0x.\n");
93
+ continue;
94
+ }
95
+ output.write("Enter 1 or 2.\n");
96
+ }
97
+ }
98
+ async function promptLlmProvider() {
99
+ while (true) {
100
+ const provider = (await prompt("Agent LLM provider (openai/anthropic/google/deepseek/mistral/groq/together/ollama): ")).toLowerCase();
101
+ if (["openai", "anthropic", "google", "deepseek", "mistral", "groq", "together", "ollama"].includes(provider)) {
102
+ return provider;
103
+ }
104
+ output.write("Unsupported provider.\n");
105
+ }
106
+ }
107
+ async function consumeBootstrapPackage(config) {
108
+ if (!config.secrets?.bootstrapToken) {
109
+ return { apiToken: "" };
110
+ }
111
+ const res = await fetch(`${config.apiUrl}/v1/agents/bootstrap/consume`, {
112
+ method: "POST",
113
+ headers: { "Content-Type": "application/json" },
114
+ body: JSON.stringify({
115
+ agentId: config.agentId,
116
+ token: config.secrets.bootstrapToken
117
+ })
118
+ });
119
+ if (!res.ok) {
120
+ const body = await res.text();
121
+ throw new Error(`Failed to consume secure setup package: ${body}`);
122
+ }
123
+ const data = await res.json();
124
+ return data.payload;
125
+ }
126
+ async function buildLocalSecrets(config, bootstrapPayload) {
127
+ const nextConfig = structuredClone(config);
128
+ const llm = { ...nextConfig.llm || {} };
129
+ if (bootstrapPayload.llm?.provider && !llm.provider) {
130
+ llm.provider = bootstrapPayload.llm.provider;
131
+ }
132
+ if (bootstrapPayload.llm?.model && !llm.model) {
133
+ llm.model = bootstrapPayload.llm.model;
134
+ }
135
+ if (!llm.provider) {
136
+ llm.provider = await promptLlmProvider();
137
+ }
138
+ if (!llm.model) {
139
+ llm.model = await prompt("Agent LLM model: ");
140
+ if (!llm.model) {
141
+ throw new Error("Agent LLM model is required");
142
+ }
143
+ }
144
+ const secrets = {
145
+ apiToken: bootstrapPayload.apiToken || nextConfig.apiToken || await promptSecret("Agent relay token: "),
146
+ walletPrivateKey: bootstrapPayload.walletPrivateKey || nextConfig.wallet?.privateKey || await promptWalletPrivateKey(),
147
+ llmApiKey: bootstrapPayload.llm?.apiKey || nextConfig.llm.apiKey || await promptSecret("Agent LLM API key: ")
148
+ };
149
+ if (!secrets.apiToken) {
150
+ throw new Error("Agent relay token is required");
151
+ }
152
+ nextConfig.llm = llm;
153
+ delete nextConfig.apiToken;
154
+ delete nextConfig.wallet;
155
+ delete nextConfig.llm.apiKey;
156
+ return { config: nextConfig, secrets };
157
+ }
158
+ function writeSecureStore(path, secrets, password) {
159
+ const secureStorePath = expandHomeDir(path);
160
+ const encrypted = encryptSecretPayload(secrets, password);
161
+ const dir = dirname(secureStorePath);
162
+ if (!existsSync(dir)) {
163
+ mkdirSync(dir, { recursive: true, mode: 448 });
164
+ }
165
+ writeFileSync(secureStorePath, JSON.stringify(encrypted, null, 2), { mode: 384 });
166
+ try {
167
+ chmodSync(secureStorePath, 384);
168
+ } catch {
169
+ }
170
+ return secureStorePath;
171
+ }
172
+ async function ensureLocalSetup(configPath) {
173
+ const config = readConfigFile(configPath);
174
+ const existingSecureStorePath = config.secrets?.secureStorePath ? expandHomeDir(config.secrets.secureStorePath) : null;
175
+ if (existingSecureStorePath && !config.secrets?.bootstrapToken && existsSync(existingSecureStorePath) && !config.apiToken && !config.wallet?.privateKey && !config.llm.apiKey) {
176
+ return;
177
+ }
178
+ const bootstrapPayload = await consumeBootstrapPackage(config);
179
+ const { config: nextConfig, secrets } = await buildLocalSecrets(config, bootstrapPayload);
180
+ const password = await promptPasswordWithConfirmation();
181
+ const secureStorePath = writeSecureStore(
182
+ nextConfig.secrets?.secureStorePath || getDefaultSecureStorePath(nextConfig.agentId),
183
+ secrets,
184
+ password
185
+ );
186
+ nextConfig.secrets = {
187
+ secureStorePath
188
+ };
189
+ writeConfigFile(configPath, nextConfig);
190
+ output.write(`Encrypted local secret store created at ${secureStorePath}
191
+ `);
192
+ }
193
+
194
+ // src/cli.ts
11
195
  var program = new Command();
12
196
  program.name("exagent").description("Exagent \u2014 LLM trading agent runtime").version("0.1.0");
13
197
  program.command("init").description("Create a sample agent configuration file").option("--agent-id <id>", "Agent ID (from dashboard)", "my-agent").option("--api-url <url>", "API server URL", "http://localhost:3002").option("--config <path>", "Config file path", "agent-config.json").action((opts) => {
14
198
  writeSampleConfig(opts.agentId, opts.apiUrl, opts.config);
15
199
  console.log(`Created ${opts.config}`);
16
- console.log("Edit the file with your API token, LLM key, and strategy settings.");
17
- console.log("Then run: exagent run");
200
+ console.log("Edit only the public strategy/venue/risk settings in the file.");
201
+ console.log(`Then run: exagent setup --config ${opts.config}`);
202
+ });
203
+ program.command("setup").description("Run first-time secure local setup for agent secrets").option("--config <path>", "Config file path", "agent-config.json").action(async (opts) => {
204
+ try {
205
+ await ensureLocalSetup(opts.config);
206
+ console.log("Secure local setup complete.");
207
+ } catch (err) {
208
+ console.error("Failed to complete secure local setup:", err.message);
209
+ process.exit(1);
210
+ }
18
211
  });
19
212
  program.command("run").description("Start the agent").option("--config <path>", "Config file path", "agent-config.json").action(async (opts) => {
20
213
  try {
21
- const config = loadConfig(opts.config);
214
+ await ensureLocalSetup(opts.config);
215
+ const config = await loadConfig(opts.config, {
216
+ getSecretPassword: async () => promptSecretPassword()
217
+ });
22
218
  const runtime = new AgentRuntime(config);
23
219
  await runtime.start();
24
220
  await new Promise(() => {
@@ -40,7 +236,10 @@ program.command("templates").description("List available strategy templates").ac
40
236
  });
41
237
  program.command("status").description("Check agent status").option("--config <path>", "Config file path", "agent-config.json").action(async (opts) => {
42
238
  try {
43
- const config = loadConfig(opts.config);
239
+ await ensureLocalSetup(opts.config);
240
+ const config = await loadConfig(opts.config, {
241
+ getSecretPassword: async () => promptSecretPassword()
242
+ });
44
243
  const res = await fetch(`${config.apiUrl}/v1/agents/${config.agentId}`, {
45
244
  headers: { Authorization: `Bearer ${config.apiToken}` }
46
245
  });
package/dist/index.d.ts CHANGED
@@ -20,6 +20,11 @@ interface RuntimeConfig {
20
20
  strategy: {
21
21
  file?: string;
22
22
  template?: string;
23
+ prompt?: {
24
+ name?: string;
25
+ systemPrompt: string;
26
+ venues?: string[];
27
+ };
23
28
  };
24
29
  trading: {
25
30
  mode: 'live' | 'paper';
@@ -78,7 +83,10 @@ interface RuntimeConfig {
78
83
  json?: boolean;
79
84
  };
80
85
  }
81
- declare function loadConfig(path?: string): RuntimeConfig;
86
+ interface LoadConfigOptions {
87
+ getSecretPassword?: () => Promise<string>;
88
+ }
89
+ declare function loadConfig(path?: string, options?: LoadConfigOptions): Promise<RuntimeConfig>;
82
90
  declare function generateSampleConfig(agentId: string, apiUrl: string): string;
83
91
  declare function writeSampleConfig(agentId: string, apiUrl: string, path?: string): void;
84
92
 
@@ -408,6 +416,11 @@ declare function createLLMAdapter(config: LLMConfig): LLMAdapter;
408
416
  declare function loadStrategy(config: {
409
417
  file?: string;
410
418
  template?: string;
419
+ prompt?: {
420
+ name?: string;
421
+ systemPrompt: string;
422
+ venues?: string[];
423
+ };
411
424
  }): Promise<StrategyFunction>;
412
425
 
413
426
  declare function getTemplate(id: string): StrategyTemplate | undefined;
package/dist/index.js CHANGED
@@ -43,7 +43,7 @@ import {
43
43
  loadConfig,
44
44
  loadStrategy,
45
45
  writeSampleConfig
46
- } from "./chunk-B4VHIITU.js";
46
+ } from "./chunk-UAP5CTHB.js";
47
47
  export {
48
48
  AcrossAdapter,
49
49
  AerodromeAdapter,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exagent/agent",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/cli.ts CHANGED
@@ -3,6 +3,7 @@ import { Command } from 'commander';
3
3
  import { loadConfig, writeSampleConfig } from './config.js';
4
4
  import { AgentRuntime } from './runtime.js';
5
5
  import { listTemplates } from './strategy/index.js';
6
+ import { ensureLocalSetup, promptSecretPassword } from './setup.js';
6
7
 
7
8
  const program = new Command();
8
9
 
@@ -20,8 +21,22 @@ program
20
21
  .action((opts) => {
21
22
  writeSampleConfig(opts.agentId, opts.apiUrl, opts.config);
22
23
  console.log(`Created ${opts.config}`);
23
- console.log('Edit the file with your API token, LLM key, and strategy settings.');
24
- console.log('Then run: exagent run');
24
+ console.log('Edit only the public strategy/venue/risk settings in the file.');
25
+ console.log(`Then run: exagent setup --config ${opts.config}`);
26
+ });
27
+
28
+ program
29
+ .command('setup')
30
+ .description('Run first-time secure local setup for agent secrets')
31
+ .option('--config <path>', 'Config file path', 'agent-config.json')
32
+ .action(async (opts) => {
33
+ try {
34
+ await ensureLocalSetup(opts.config);
35
+ console.log('Secure local setup complete.');
36
+ } catch (err) {
37
+ console.error('Failed to complete secure local setup:', (err as Error).message);
38
+ process.exit(1);
39
+ }
25
40
  });
26
41
 
27
42
  program
@@ -30,7 +45,10 @@ program
30
45
  .option('--config <path>', 'Config file path', 'agent-config.json')
31
46
  .action(async (opts) => {
32
47
  try {
33
- const config = loadConfig(opts.config);
48
+ await ensureLocalSetup(opts.config);
49
+ const config = await loadConfig(opts.config, {
50
+ getSecretPassword: async () => promptSecretPassword(),
51
+ });
34
52
  const runtime = new AgentRuntime(config);
35
53
  await runtime.start();
36
54
 
@@ -62,7 +80,10 @@ program
62
80
  .option('--config <path>', 'Config file path', 'agent-config.json')
63
81
  .action(async (opts) => {
64
82
  try {
65
- const config = loadConfig(opts.config);
83
+ await ensureLocalSetup(opts.config);
84
+ const config = await loadConfig(opts.config, {
85
+ getSecretPassword: async () => promptSecretPassword(),
86
+ });
66
87
  const res = await fetch(`${config.apiUrl}/v1/agents/${config.agentId}`, {
67
88
  headers: { Authorization: `Bearer ${config.apiToken}` },
68
89
  });
package/src/config.ts CHANGED
@@ -1,4 +1,7 @@
1
- import { readFileSync, existsSync, writeFileSync } from 'node:fs';
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
 
@@ -20,6 +23,11 @@ export interface RuntimeConfig {
20
23
  strategy: {
21
24
  file?: string;
22
25
  template?: string;
26
+ prompt?: {
27
+ name?: string;
28
+ systemPrompt: string;
29
+ venues?: string[];
30
+ };
23
31
  };
24
32
  trading: {
25
33
  mode: 'live' | 'paper';
@@ -79,15 +87,43 @@ export interface RuntimeConfig {
79
87
  };
80
88
  }
81
89
 
82
- const configSchema = z.object({
90
+ export interface LocalSecretPayload {
91
+ apiToken: string;
92
+ walletPrivateKey?: string;
93
+ llmApiKey?: string;
94
+ }
95
+
96
+ export interface SecureStoreFile {
97
+ version: 1;
98
+ algorithm: 'aes-256-gcm';
99
+ kdf: {
100
+ name: 'scrypt';
101
+ salt: string;
102
+ keyLength: 32;
103
+ cost: number;
104
+ blockSize: number;
105
+ parallelization: number;
106
+ };
107
+ iv: string;
108
+ ciphertext: string;
109
+ authTag: string;
110
+ }
111
+
112
+ export interface LoadConfigOptions {
113
+ getSecretPassword?: () => Promise<string>;
114
+ }
115
+
116
+ const providerEnum = z.enum(['openai', 'anthropic', 'google', 'deepseek', 'mistral', 'groq', 'together', 'ollama']);
117
+
118
+ const runtimeSchema = z.object({
83
119
  agentId: z.string(),
84
120
  apiUrl: z.string().url(),
85
- apiToken: z.string(),
121
+ apiToken: z.string().min(1),
86
122
  wallet: z.object({
87
- privateKey: z.string(),
123
+ privateKey: z.string().regex(/^0x[a-fA-F0-9]{64}$/),
88
124
  }).optional(),
89
125
  llm: z.object({
90
- provider: z.enum(['openai', 'anthropic', 'google', 'deepseek', 'mistral', 'groq', 'together', 'ollama']),
126
+ provider: providerEnum,
91
127
  model: z.string().optional(),
92
128
  apiKey: z.string().optional(),
93
129
  endpoint: z.string().optional(),
@@ -97,6 +133,11 @@ const configSchema = z.object({
97
133
  strategy: z.object({
98
134
  file: z.string().optional(),
99
135
  template: z.string().optional(),
136
+ prompt: z.object({
137
+ name: z.string().optional(),
138
+ systemPrompt: z.string().min(1),
139
+ venues: z.array(z.string()).optional(),
140
+ }).optional(),
100
141
  }),
101
142
  trading: z.object({
102
143
  mode: z.enum(['live', 'paper']).default('paper'),
@@ -156,23 +197,172 @@ const configSchema = z.object({
156
197
  }).optional(),
157
198
  });
158
199
 
159
- export function loadConfig(path: string = 'agent-config.json'): RuntimeConfig {
200
+ const configFileSchema = z.object({
201
+ agentId: z.string(),
202
+ apiUrl: z.string().url(),
203
+ apiToken: z.string().min(1).optional(),
204
+ wallet: z.object({
205
+ privateKey: z.string().regex(/^0x[a-fA-F0-9]{64}$/),
206
+ }).optional(),
207
+ llm: z.object({
208
+ provider: providerEnum.optional(),
209
+ model: z.string().optional(),
210
+ apiKey: z.string().optional(),
211
+ endpoint: z.string().optional(),
212
+ temperature: z.number().min(0).max(2).optional(),
213
+ maxTokens: z.number().optional(),
214
+ }).default({}),
215
+ strategy: z.object({
216
+ file: z.string().optional(),
217
+ template: z.string().optional(),
218
+ prompt: z.object({
219
+ name: z.string().optional(),
220
+ systemPrompt: z.string().min(1),
221
+ venues: z.array(z.string()).optional(),
222
+ }).optional(),
223
+ }),
224
+ trading: runtimeSchema.shape.trading,
225
+ venues: runtimeSchema.shape.venues,
226
+ relay: runtimeSchema.shape.relay,
227
+ llmBudget: runtimeSchema.shape.llmBudget,
228
+ rpcOverrides: runtimeSchema.shape.rpcOverrides,
229
+ logging: runtimeSchema.shape.logging,
230
+ secrets: z.object({
231
+ bootstrapToken: z.string().optional(),
232
+ bootstrapExpiresAt: z.string().optional(),
233
+ secureStorePath: z.string().optional(),
234
+ }).optional(),
235
+ });
236
+
237
+ const secureStoreSchema = z.object({
238
+ version: z.literal(1),
239
+ algorithm: z.literal('aes-256-gcm'),
240
+ kdf: z.object({
241
+ name: z.literal('scrypt'),
242
+ salt: z.string(),
243
+ keyLength: z.literal(32),
244
+ cost: z.number().int().positive(),
245
+ blockSize: z.number().int().positive(),
246
+ parallelization: z.number().int().positive(),
247
+ }),
248
+ iv: z.string(),
249
+ ciphertext: z.string(),
250
+ authTag: z.string(),
251
+ });
252
+
253
+ export type RuntimeConfigFile = z.infer<typeof configFileSchema>;
254
+
255
+ const DEFAULT_SCRYPT_COST = 32768;
256
+ const DEFAULT_SCRYPT_BLOCK_SIZE = 8;
257
+ const DEFAULT_SCRYPT_PARALLELIZATION = 1;
258
+
259
+ function expandHomeDir(path: string): string {
260
+ if (!path.startsWith('~/')) return path;
261
+ return resolve(homedir(), path.slice(2));
262
+ }
263
+
264
+ export function getDefaultSecureStorePath(agentId: string): string {
265
+ return resolve(homedir(), '.exagent', 'agents', agentId, 'secrets.json');
266
+ }
267
+
268
+ export function readConfigFile(path: string = 'agent-config.json'): RuntimeConfigFile {
160
269
  if (!existsSync(path)) {
161
270
  throw new Error(`Config file not found: ${path}. Run 'exagent init' first.`);
162
271
  }
163
272
 
164
- const raw = readFileSync(path, 'utf-8');
165
273
  let parsed: unknown;
166
-
167
274
  try {
168
- parsed = JSON.parse(raw);
275
+ parsed = JSON.parse(readFileSync(path, 'utf-8'));
169
276
  } catch {
170
277
  throw new Error(`Invalid JSON in ${path}`);
171
278
  }
172
279
 
173
- // Apply environment variable overrides
174
- const config = parsed as Record<string, unknown>;
175
- const llm = (config.llm || {}) as Record<string, unknown>;
280
+ const result = configFileSchema.safeParse(parsed);
281
+ if (!result.success) {
282
+ const issues = result.error.issues.map((issue) => ` ${issue.path.join('.')}: ${issue.message}`).join('\n');
283
+ throw new Error(`Invalid config file:\n${issues}`);
284
+ }
285
+
286
+ return result.data;
287
+ }
288
+
289
+ export function writeConfigFile(path: string, config: RuntimeConfigFile): void {
290
+ writeFileSync(path, JSON.stringify(config, null, 2));
291
+ }
292
+
293
+ export function encryptSecretPayload(payload: LocalSecretPayload, password: string): SecureStoreFile {
294
+ const salt = randomBytes(16);
295
+ const iv = randomBytes(12);
296
+ const key = scryptSync(password, salt, 32, {
297
+ N: DEFAULT_SCRYPT_COST,
298
+ r: DEFAULT_SCRYPT_BLOCK_SIZE,
299
+ p: DEFAULT_SCRYPT_PARALLELIZATION,
300
+ });
301
+
302
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
303
+ const plaintext = Buffer.from(JSON.stringify(payload), 'utf8');
304
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
305
+
306
+ return {
307
+ version: 1,
308
+ algorithm: 'aes-256-gcm',
309
+ kdf: {
310
+ name: 'scrypt',
311
+ salt: salt.toString('hex'),
312
+ keyLength: 32,
313
+ cost: DEFAULT_SCRYPT_COST,
314
+ blockSize: DEFAULT_SCRYPT_BLOCK_SIZE,
315
+ parallelization: DEFAULT_SCRYPT_PARALLELIZATION,
316
+ },
317
+ iv: iv.toString('hex'),
318
+ ciphertext: ciphertext.toString('hex'),
319
+ authTag: cipher.getAuthTag().toString('hex'),
320
+ };
321
+ }
322
+
323
+ export function decryptSecretPayload(path: string, password: string): LocalSecretPayload {
324
+ const secureStorePath = expandHomeDir(path);
325
+ if (!existsSync(secureStorePath)) {
326
+ throw new Error(`Encrypted secret store not found: ${secureStorePath}`);
327
+ }
328
+
329
+ let parsed: unknown;
330
+ try {
331
+ parsed = JSON.parse(readFileSync(secureStorePath, 'utf-8'));
332
+ } catch {
333
+ throw new Error(`Invalid JSON in encrypted secret store: ${secureStorePath}`);
334
+ }
335
+
336
+ const result = secureStoreSchema.safeParse(parsed);
337
+ if (!result.success) {
338
+ const issues = result.error.issues.map((issue) => ` ${issue.path.join('.')}: ${issue.message}`).join('\n');
339
+ throw new Error(`Invalid encrypted secret store:\n${issues}`);
340
+ }
341
+
342
+ const store = result.data;
343
+ const key = scryptSync(password, Buffer.from(store.kdf.salt, 'hex'), store.kdf.keyLength, {
344
+ N: store.kdf.cost,
345
+ r: store.kdf.blockSize,
346
+ p: store.kdf.parallelization,
347
+ });
348
+
349
+ try {
350
+ const decipher = createDecipheriv(store.algorithm, key, Buffer.from(store.iv, 'hex'));
351
+ decipher.setAuthTag(Buffer.from(store.authTag, 'hex'));
352
+ const plaintext = Buffer.concat([
353
+ decipher.update(Buffer.from(store.ciphertext, 'hex')),
354
+ decipher.final(),
355
+ ]);
356
+ return JSON.parse(plaintext.toString('utf8')) as LocalSecretPayload;
357
+ } catch {
358
+ throw new Error('Unable to decrypt local secret store. Check your password.');
359
+ }
360
+ }
361
+
362
+ export async function loadConfig(path: string = 'agent-config.json', options: LoadConfigOptions = {}): Promise<RuntimeConfig> {
363
+ const parsed = readConfigFile(path);
364
+ const config = structuredClone(parsed) as RuntimeConfigFile & Record<string, unknown>;
365
+ const llm = { ...(config.llm || {}) } as Record<string, unknown>;
176
366
 
177
367
  if (process.env.EXAGENT_LLM_PROVIDER) llm.provider = process.env.EXAGENT_LLM_PROVIDER;
178
368
  if (process.env.EXAGENT_LLM_MODEL) llm.model = process.env.EXAGENT_LLM_MODEL;
@@ -183,9 +373,24 @@ export function loadConfig(path: string = 'agent-config.json'): RuntimeConfig {
183
373
  config.wallet = { privateKey: process.env.EXAGENT_WALLET_PRIVATE_KEY };
184
374
  }
185
375
 
376
+ if ((!config.apiToken || !llm.apiKey || !config.wallet) && parsed.secrets?.secureStorePath) {
377
+ const password = process.env.EXAGENT_SECRET_PASSWORD || await options.getSecretPassword?.();
378
+ if (!password) {
379
+ throw new Error('Encrypted secret store found, but no password was provided.');
380
+ }
381
+
382
+ const secrets = decryptSecretPayload(parsed.secrets.secureStorePath, password);
383
+ if (!config.apiToken) config.apiToken = secrets.apiToken;
384
+ if (!config.wallet && secrets.walletPrivateKey) config.wallet = { privateKey: secrets.walletPrivateKey };
385
+ if (!llm.apiKey && secrets.llmApiKey) llm.apiKey = secrets.llmApiKey;
386
+ }
387
+
388
+ if ((!config.apiToken || !llm.apiKey || !config.wallet) && parsed.secrets?.bootstrapToken && !parsed.secrets?.secureStorePath) {
389
+ throw new Error(`Config ${path} still requires first-time secure setup. Run 'exagent setup --config ${path}' or start the agent interactively.`);
390
+ }
391
+
186
392
  config.llm = llm;
187
393
 
188
- // Apply RPC endpoint overrides from environment
189
394
  const rpcOverrides: Record<string, string> = (config.rpcOverrides as Record<string, string>) || {};
190
395
  if (process.env.EXAGENT_RPC_BASE) rpcOverrides.base = process.env.EXAGENT_RPC_BASE;
191
396
  if (process.env.EXAGENT_RPC_ARBITRUM) rpcOverrides.arbitrum = process.env.EXAGENT_RPC_ARBITRUM;
@@ -195,9 +400,9 @@ export function loadConfig(path: string = 'agent-config.json'): RuntimeConfig {
195
400
  config.rpcOverrides = rpcOverrides;
196
401
  }
197
402
 
198
- const result = configSchema.safeParse(config);
403
+ const result = runtimeSchema.safeParse(config);
199
404
  if (!result.success) {
200
- const issues = result.error.issues.map(i => ` ${i.path.join('.')}: ${i.message}`).join('\n');
405
+ const issues = result.error.issues.map((issue) => ` ${issue.path.join('.')}: ${issue.message}`).join('\n');
201
406
  throw new Error(`Invalid config:\n${issues}`);
202
407
  }
203
408
 
@@ -205,18 +410,10 @@ export function loadConfig(path: string = 'agent-config.json'): RuntimeConfig {
205
410
  }
206
411
 
207
412
  export function generateSampleConfig(agentId: string, apiUrl: string): string {
208
- const config = {
413
+ const config: RuntimeConfigFile = {
209
414
  agentId,
210
415
  apiUrl,
211
- apiToken: '<your-jwt-token>',
212
- wallet: {
213
- privateKey: '<your-hex-private-key>',
214
- },
215
- llm: {
216
- provider: 'openai',
217
- model: 'gpt-4o',
218
- apiKey: '<your-api-key>',
219
- },
416
+ llm: {},
220
417
  strategy: {
221
418
  template: 'momentum',
222
419
  },
@@ -266,6 +463,9 @@ export function generateSampleConfig(agentId: string, apiUrl: string): string {
266
463
  heartbeatIntervalMs: 30000,
267
464
  reconnectMaxAttempts: 50,
268
465
  },
466
+ secrets: {
467
+ secureStorePath: getDefaultSecureStorePath(agentId),
468
+ },
269
469
  };
270
470
 
271
471
  return JSON.stringify(config, null, 2);