@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/.turbo/turbo-build.log +4 -4
- package/dist/chunk-TDACLKD7.js +5867 -0
- package/dist/chunk-UAP5CTHB.js +5985 -0
- package/dist/cli.js +204 -5
- package/dist/index.d.ts +14 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli.ts +25 -4
- package/src/config.ts +225 -25
- package/src/setup.ts +233 -0
- package/src/strategy/loader.ts +98 -0
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-
|
|
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
|
|
17
|
-
console.log(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
package/package.json
CHANGED
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
|
|
24
|
-
console.log(
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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 =
|
|
403
|
+
const result = runtimeSchema.safeParse(config);
|
|
199
404
|
if (!result.success) {
|
|
200
|
-
const issues = result.error.issues.map(
|
|
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
|
-
|
|
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);
|