morpheus-cli 0.6.5 → 0.6.6
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 +7 -1
- package/dist/channels/discord.js +3 -2
- package/dist/channels/telegram.js +3 -2
- package/dist/cli/commands/start.js +30 -0
- package/dist/config/manager.js +117 -1
- package/dist/config/precedence.js +107 -0
- package/dist/http/api.js +29 -0
- package/dist/runtime/providers/factory.js +7 -5
- package/dist/runtime/trinity-crypto.js +51 -0
- package/dist/ui/assets/index-B9BClunx.js +111 -0
- package/dist/ui/assets/index-B9ngtbja.css +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/ui/sw.js +1 -1
- package/package.json +1 -1
- package/dist/ui/assets/index-BDWWF6gM.css +0 -1
- package/dist/ui/assets/index-DTUgtIcM.js +0 -111
package/README.md
CHANGED
|
@@ -82,8 +82,11 @@ Minimal `env.docker`:
|
|
|
82
82
|
```env
|
|
83
83
|
OPENAI_API_KEY=sk-...
|
|
84
84
|
THE_ARCHITECT_PASS=changeme
|
|
85
|
+
MORPHEUS_SECRET=<generate-a-random-secret>
|
|
85
86
|
```
|
|
86
87
|
|
|
88
|
+
> **Tip:** Generate a secure `MORPHEUS_SECRET` with: `openssl rand -base64 32` or `node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"`
|
|
89
|
+
|
|
87
90
|
**2. Run:**
|
|
88
91
|
|
|
89
92
|
```bash
|
|
@@ -121,6 +124,7 @@ docker run -d \
|
|
|
121
124
|
-p 3333:3333 \
|
|
122
125
|
-e OPENAI_API_KEY=sk-... \
|
|
123
126
|
-e THE_ARCHITECT_PASS=changeme \
|
|
127
|
+
-e MORPHEUS_SECRET=<generate-a-random-secret> \
|
|
124
128
|
-v morpheus_data:/root/.morpheus \
|
|
125
129
|
morpheus
|
|
126
130
|
```
|
|
@@ -133,6 +137,7 @@ docker run -d \
|
|
|
133
137
|
-p 3333:3333 \
|
|
134
138
|
-e OPENAI_API_KEY=sk-... \
|
|
135
139
|
-e THE_ARCHITECT_PASS=changeme \
|
|
140
|
+
-e MORPHEUS_SECRET=<generate-a-random-secret> \
|
|
136
141
|
-e MORPHEUS_TELEGRAM_ENABLED=true \
|
|
137
142
|
-e MORPHEUS_TELEGRAM_TOKEN=<bot-token> \
|
|
138
143
|
-e MORPHEUS_TELEGRAM_ALLOWED_USERS=123456789 \
|
|
@@ -148,6 +153,7 @@ docker run -d \
|
|
|
148
153
|
-p 3333:3333 \
|
|
149
154
|
-e OPENAI_API_KEY=sk-... \
|
|
150
155
|
-e THE_ARCHITECT_PASS=changeme \
|
|
156
|
+
-e MORPHEUS_SECRET=<generate-a-random-secret> \
|
|
151
157
|
-e MORPHEUS_DISCORD_ENABLED=true \
|
|
152
158
|
-e MORPHEUS_DISCORD_TOKEN=<bot-token> \
|
|
153
159
|
-e MORPHEUS_DISCORD_ALLOWED_USERS=987654321 \
|
|
@@ -365,7 +371,7 @@ Provider-specific keys:
|
|
|
365
371
|
- `THE_ARCHITECT_PASS`
|
|
366
372
|
|
|
367
373
|
Security:
|
|
368
|
-
- `MORPHEUS_SECRET` — AES-256-GCM key for
|
|
374
|
+
- `MORPHEUS_SECRET` — AES-256-GCM encryption key for Trinity database passwords and agent API keys. When set, all API keys saved via UI or config file are automatically encrypted at rest.
|
|
369
375
|
|
|
370
376
|
Generic Morpheus overrides (selected):
|
|
371
377
|
|
package/dist/channels/discord.js
CHANGED
|
@@ -7,6 +7,7 @@ import { SQLiteChatMessageHistory } from '../runtime/memory/sqlite.js';
|
|
|
7
7
|
import { DisplayManager } from '../runtime/display.js';
|
|
8
8
|
import { ConfigManager } from '../config/manager.js';
|
|
9
9
|
import { createTelephonist } from '../runtime/telephonist.js';
|
|
10
|
+
import { getUsableApiKey } from '../runtime/trinity-crypto.js';
|
|
10
11
|
// ─── Slash Command Definitions ────────────────────────────────────────────────
|
|
11
12
|
const SLASH_COMMANDS = [
|
|
12
13
|
new SlashCommandBuilder()
|
|
@@ -219,8 +220,8 @@ export class DiscordAdapter {
|
|
|
219
220
|
await channel.send('Audio transcription is currently disabled.');
|
|
220
221
|
return;
|
|
221
222
|
}
|
|
222
|
-
const apiKey = config.audio.apiKey ||
|
|
223
|
-
(config.llm.provider === config.audio.provider ? config.llm.api_key : undefined);
|
|
223
|
+
const apiKey = getUsableApiKey(config.audio.apiKey) ||
|
|
224
|
+
(config.llm.provider === config.audio.provider ? getUsableApiKey(config.llm.api_key) : undefined);
|
|
224
225
|
if (!apiKey) {
|
|
225
226
|
this.display.log(`Audio transcription failed: No API key for provider '${config.audio.provider}'`, { source: 'Telephonist', level: 'error' });
|
|
226
227
|
await channel.send(`Audio transcription requires an API key for provider '${config.audio.provider}'.`);
|
|
@@ -8,6 +8,7 @@ import { spawn } from 'child_process';
|
|
|
8
8
|
import { ConfigManager } from '../config/manager.js';
|
|
9
9
|
import { DisplayManager } from '../runtime/display.js';
|
|
10
10
|
import { createTelephonist } from '../runtime/telephonist.js';
|
|
11
|
+
import { getUsableApiKey } from '../runtime/trinity-crypto.js';
|
|
11
12
|
import { readPid, isProcessRunning, checkStalePid } from '../runtime/lifecycle.js';
|
|
12
13
|
import { SQLiteChatMessageHistory } from '../runtime/memory/sqlite.js';
|
|
13
14
|
import { SatiRepository } from '../runtime/memory/sati/repository.js';
|
|
@@ -281,8 +282,8 @@ export class TelegramAdapter {
|
|
|
281
282
|
await ctx.reply("Audio transcription is currently disabled.");
|
|
282
283
|
return;
|
|
283
284
|
}
|
|
284
|
-
const apiKey = config.audio.apiKey ||
|
|
285
|
-
(config.llm.provider === config.audio.provider ? config.llm.api_key : undefined);
|
|
285
|
+
const apiKey = getUsableApiKey(config.audio.apiKey) ||
|
|
286
|
+
(config.llm.provider === config.audio.provider ? getUsableApiKey(config.llm.api_key) : undefined);
|
|
286
287
|
if (!apiKey) {
|
|
287
288
|
this.display.log(`Audio transcription failed: No API key available for provider '${config.audio.provider}'`, { source: 'Telephonist', level: 'error' });
|
|
288
289
|
await ctx.reply(`Audio transcription requires an API key for provider '${config.audio.provider}'. Please configure \`audio.apiKey\` or use the same provider as your LLM.`);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
|
+
import path from 'path';
|
|
4
5
|
import { confirm } from '@inquirer/prompts';
|
|
5
6
|
import { scaffold } from '../../runtime/scaffold.js';
|
|
6
7
|
import { DisplayManager } from '../../runtime/display.js';
|
|
@@ -21,6 +22,35 @@ import { TaskWorker } from '../../runtime/tasks/worker.js';
|
|
|
21
22
|
import { TaskNotifier } from '../../runtime/tasks/notifier.js';
|
|
22
23
|
import { ChronosWorker } from '../../runtime/chronos/worker.js';
|
|
23
24
|
import { ChronosRepository } from '../../runtime/chronos/repository.js';
|
|
25
|
+
// Load .env file explicitly in start command
|
|
26
|
+
const envPath = path.join(process.cwd(), '.env');
|
|
27
|
+
if (fs.existsSync(envPath)) {
|
|
28
|
+
try {
|
|
29
|
+
const envConfig = fs.readFileSync(envPath, 'utf-8');
|
|
30
|
+
envConfig.split('\n').forEach(line => {
|
|
31
|
+
const trimmed = line.trim();
|
|
32
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
33
|
+
return;
|
|
34
|
+
const match = trimmed.match(/^([^=]+)=(.*)$/);
|
|
35
|
+
if (match) {
|
|
36
|
+
const key = match[1].trim();
|
|
37
|
+
let value = match[2].trim();
|
|
38
|
+
// Remove quotes if present
|
|
39
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
40
|
+
value = value.slice(1, -1);
|
|
41
|
+
}
|
|
42
|
+
// Don't overwrite existing env vars
|
|
43
|
+
if (!process.env[key]) {
|
|
44
|
+
process.env[key] = value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
console.log('[DEBUG] Loaded .env file from:', envPath);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
console.error('[WARN] Failed to load .env:', err);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
24
54
|
export const startCommand = new Command('start')
|
|
25
55
|
.description('Start the Morpheus agent')
|
|
26
56
|
.option('--ui', 'Enable web UI', true)
|
package/dist/config/manager.js
CHANGED
|
@@ -6,6 +6,8 @@ import { setByPath } from './utils.js';
|
|
|
6
6
|
import { ConfigSchema } from './schemas.js';
|
|
7
7
|
import { migrateConfigFile } from '../runtime/migration.js';
|
|
8
8
|
import { resolveApiKey, resolveModel, resolveNumeric, resolveString, resolveBoolean, resolveProvider, resolveStringArray } from './precedence.js';
|
|
9
|
+
import { encrypt, safeDecrypt, looksLikeEncrypted, canEncrypt } from '../runtime/trinity-crypto.js';
|
|
10
|
+
import { DisplayManager } from '../runtime/display.js';
|
|
9
11
|
export class ConfigManager {
|
|
10
12
|
static instance;
|
|
11
13
|
config = DEFAULT_CONFIG;
|
|
@@ -16,6 +18,89 @@ export class ConfigManager {
|
|
|
16
18
|
}
|
|
17
19
|
return ConfigManager.instance;
|
|
18
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Decrypts API keys in config if they appear to be encrypted.
|
|
23
|
+
* Fail-open: returns original value if decryption fails.
|
|
24
|
+
*/
|
|
25
|
+
decryptAgentApiKeys(config) {
|
|
26
|
+
const decrypted = { ...config };
|
|
27
|
+
// Helper to decrypt a single key
|
|
28
|
+
const tryDecrypt = (apiKey) => {
|
|
29
|
+
if (!apiKey)
|
|
30
|
+
return undefined;
|
|
31
|
+
if (!looksLikeEncrypted(apiKey))
|
|
32
|
+
return apiKey;
|
|
33
|
+
const decrypted = safeDecrypt(apiKey);
|
|
34
|
+
return decrypted ?? apiKey; // Return original if decryption fails
|
|
35
|
+
};
|
|
36
|
+
// Decrypt Oracle/LLM
|
|
37
|
+
if (decrypted.llm.api_key) {
|
|
38
|
+
decrypted.llm = { ...decrypted.llm, api_key: tryDecrypt(decrypted.llm.api_key) };
|
|
39
|
+
}
|
|
40
|
+
// Decrypt Sati
|
|
41
|
+
if (decrypted.sati?.api_key) {
|
|
42
|
+
decrypted.sati = { ...decrypted.sati, api_key: tryDecrypt(decrypted.sati.api_key) };
|
|
43
|
+
}
|
|
44
|
+
// Decrypt Apoc
|
|
45
|
+
if (decrypted.apoc?.api_key) {
|
|
46
|
+
decrypted.apoc = { ...decrypted.apoc, api_key: tryDecrypt(decrypted.apoc.api_key) };
|
|
47
|
+
}
|
|
48
|
+
// Decrypt Neo
|
|
49
|
+
if (decrypted.neo?.api_key) {
|
|
50
|
+
decrypted.neo = { ...decrypted.neo, api_key: tryDecrypt(decrypted.neo.api_key) };
|
|
51
|
+
}
|
|
52
|
+
// Decrypt Trinity
|
|
53
|
+
if (decrypted.trinity?.api_key) {
|
|
54
|
+
decrypted.trinity = { ...decrypted.trinity, api_key: tryDecrypt(decrypted.trinity.api_key) };
|
|
55
|
+
}
|
|
56
|
+
// Decrypt Audio (Telephonist)
|
|
57
|
+
if (decrypted.audio?.apiKey) {
|
|
58
|
+
decrypted.audio = { ...decrypted.audio, apiKey: tryDecrypt(decrypted.audio.apiKey) };
|
|
59
|
+
}
|
|
60
|
+
return decrypted;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Encrypts API keys in config if MORPHEUS_SECRET is set.
|
|
64
|
+
*/
|
|
65
|
+
encryptAgentApiKeys(config) {
|
|
66
|
+
if (!canEncrypt())
|
|
67
|
+
return config;
|
|
68
|
+
const encrypted = { ...config };
|
|
69
|
+
// Helper to encrypt a single key
|
|
70
|
+
const tryEncrypt = (apiKey) => {
|
|
71
|
+
if (!apiKey)
|
|
72
|
+
return undefined;
|
|
73
|
+
// Don't double-encrypt
|
|
74
|
+
if (looksLikeEncrypted(apiKey))
|
|
75
|
+
return apiKey;
|
|
76
|
+
return encrypt(apiKey);
|
|
77
|
+
};
|
|
78
|
+
// Encrypt Oracle/LLM
|
|
79
|
+
if (encrypted.llm.api_key) {
|
|
80
|
+
encrypted.llm = { ...encrypted.llm, api_key: tryEncrypt(encrypted.llm.api_key) };
|
|
81
|
+
}
|
|
82
|
+
// Encrypt Sati
|
|
83
|
+
if (encrypted.sati?.api_key) {
|
|
84
|
+
encrypted.sati = { ...encrypted.sati, api_key: tryEncrypt(encrypted.sati.api_key) };
|
|
85
|
+
}
|
|
86
|
+
// Encrypt Apoc
|
|
87
|
+
if (encrypted.apoc?.api_key) {
|
|
88
|
+
encrypted.apoc = { ...encrypted.apoc, api_key: tryEncrypt(encrypted.apoc.api_key) };
|
|
89
|
+
}
|
|
90
|
+
// Encrypt Neo
|
|
91
|
+
if (encrypted.neo?.api_key) {
|
|
92
|
+
encrypted.neo = { ...encrypted.neo, api_key: tryEncrypt(encrypted.neo.api_key) };
|
|
93
|
+
}
|
|
94
|
+
// Encrypt Trinity
|
|
95
|
+
if (encrypted.trinity?.api_key) {
|
|
96
|
+
encrypted.trinity = { ...encrypted.trinity, api_key: tryEncrypt(encrypted.trinity.api_key) };
|
|
97
|
+
}
|
|
98
|
+
// Encrypt Audio (Telephonist)
|
|
99
|
+
if (encrypted.audio?.apiKey) {
|
|
100
|
+
encrypted.audio = { ...encrypted.audio, apiKey: tryEncrypt(encrypted.audio.apiKey) };
|
|
101
|
+
}
|
|
102
|
+
return encrypted;
|
|
103
|
+
}
|
|
19
104
|
async load() {
|
|
20
105
|
try {
|
|
21
106
|
await migrateConfigFile();
|
|
@@ -222,9 +307,16 @@ export class ConfigManager {
|
|
|
222
307
|
}
|
|
223
308
|
async save(newConfig) {
|
|
224
309
|
// Deep merge or overwrite? simpler to overwrite for now or merge top level
|
|
225
|
-
|
|
310
|
+
let updated = { ...this.config, ...newConfig };
|
|
311
|
+
// Encrypt API keys before saving if MORPHEUS_SECRET is set
|
|
312
|
+
updated = this.encryptAgentApiKeys(updated);
|
|
226
313
|
// Validate before saving
|
|
227
314
|
const valid = ConfigSchema.parse(updated);
|
|
315
|
+
// Warn if saving without encryption
|
|
316
|
+
if (!canEncrypt()) {
|
|
317
|
+
const display = DisplayManager.getInstance();
|
|
318
|
+
display.log('API keys saved in PLAINTEXT. Set MORPHEUS_SECRET environment variable to enable AES-256-GCM encryption.', { source: 'ConfigManager', level: 'warning' });
|
|
319
|
+
}
|
|
228
320
|
await fs.ensureDir(PATHS.root);
|
|
229
321
|
await fs.writeFile(PATHS.config, yaml.dump(valid), 'utf8');
|
|
230
322
|
this.config = valid;
|
|
@@ -285,4 +377,28 @@ export class ConfigManager {
|
|
|
285
377
|
}
|
|
286
378
|
return defaults;
|
|
287
379
|
}
|
|
380
|
+
/**
|
|
381
|
+
* Returns encryption status for all agent API keys.
|
|
382
|
+
*/
|
|
383
|
+
getEncryptionStatus() {
|
|
384
|
+
const checkKey = (apiKey) => {
|
|
385
|
+
if (!apiKey)
|
|
386
|
+
return true; // No key = not plaintext
|
|
387
|
+
return looksLikeEncrypted(apiKey);
|
|
388
|
+
};
|
|
389
|
+
const apiKeysEncrypted = {
|
|
390
|
+
oracle: checkKey(this.config.llm.api_key),
|
|
391
|
+
sati: checkKey(this.config.sati?.api_key),
|
|
392
|
+
neo: checkKey(this.config.neo?.api_key),
|
|
393
|
+
apoc: checkKey(this.config.apoc?.api_key),
|
|
394
|
+
trinity: checkKey(this.config.trinity?.api_key),
|
|
395
|
+
audio: checkKey(this.config.audio?.apiKey),
|
|
396
|
+
};
|
|
397
|
+
const hasPlaintextKeys = Object.values(apiKeysEncrypted).some(encrypted => !encrypted);
|
|
398
|
+
return {
|
|
399
|
+
morpheusSecretSet: canEncrypt(),
|
|
400
|
+
apiKeysEncrypted,
|
|
401
|
+
hasPlaintextKeys,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
288
404
|
}
|
|
@@ -136,3 +136,110 @@ export function resolveProvider(genericEnvVar, configFileValue, defaultValue) {
|
|
|
136
136
|
}
|
|
137
137
|
return defaultValue;
|
|
138
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Check if a configuration value is being overridden by an environment variable.
|
|
141
|
+
* Returns true if the value comes from an environment variable (either provider-specific or generic).
|
|
142
|
+
*/
|
|
143
|
+
export function isOverriddenByEnv(provider, genericEnvVar, configFileValue) {
|
|
144
|
+
const providerSpecificVars = {
|
|
145
|
+
'openai': 'OPENAI_API_KEY',
|
|
146
|
+
'anthropic': 'ANTHROPIC_API_KEY',
|
|
147
|
+
'openrouter': 'OPENROUTER_API_KEY',
|
|
148
|
+
'ollama': '',
|
|
149
|
+
'gemini': 'GOOGLE_API_KEY'
|
|
150
|
+
};
|
|
151
|
+
const providerSpecificVar = providerSpecificVars[provider];
|
|
152
|
+
// Check if provider-specific variable is set
|
|
153
|
+
if (providerSpecificVar && process.env[providerSpecificVar]) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
// Check if generic variable is set
|
|
157
|
+
if (process.env[genericEnvVar]) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Check if a generic configuration value is being overridden by an environment variable.
|
|
164
|
+
*/
|
|
165
|
+
export function isEnvVarSet(envVarName) {
|
|
166
|
+
return process.env[envVarName] !== undefined && process.env[envVarName] !== '';
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get list of all active environment variable overrides for agent configurations.
|
|
170
|
+
*/
|
|
171
|
+
export function getActiveEnvOverrides() {
|
|
172
|
+
const overrides = {};
|
|
173
|
+
// LLM/Oracle
|
|
174
|
+
if (isEnvVarSet('MORPHEUS_LLM_PROVIDER'))
|
|
175
|
+
overrides['llm.provider'] = true;
|
|
176
|
+
if (isEnvVarSet('MORPHEUS_LLM_MODEL'))
|
|
177
|
+
overrides['llm.model'] = true;
|
|
178
|
+
if (isEnvVarSet('MORPHEUS_LLM_TEMPERATURE'))
|
|
179
|
+
overrides['llm.temperature'] = true;
|
|
180
|
+
if (isEnvVarSet('MORPHEUS_LLM_MAX_TOKENS'))
|
|
181
|
+
overrides['llm.max_tokens'] = true;
|
|
182
|
+
if (isEnvVarSet('MORPHEUS_LLM_API_KEY'))
|
|
183
|
+
overrides['llm.api_key'] = true;
|
|
184
|
+
if (isEnvVarSet('MORPHEUS_LLM_CONTEXT_WINDOW'))
|
|
185
|
+
overrides['llm.context_window'] = true;
|
|
186
|
+
// Provider-specific API keys
|
|
187
|
+
if (isEnvVarSet('OPENAI_API_KEY'))
|
|
188
|
+
overrides['llm.api_key_openai'] = true;
|
|
189
|
+
if (isEnvVarSet('ANTHROPIC_API_KEY'))
|
|
190
|
+
overrides['llm.api_key_anthropic'] = true;
|
|
191
|
+
if (isEnvVarSet('GOOGLE_API_KEY'))
|
|
192
|
+
overrides['llm.api_key_gemini'] = true;
|
|
193
|
+
if (isEnvVarSet('OPENROUTER_API_KEY'))
|
|
194
|
+
overrides['llm.api_key_openrouter'] = true;
|
|
195
|
+
// Sati
|
|
196
|
+
if (isEnvVarSet('MORPHEUS_SATI_PROVIDER'))
|
|
197
|
+
overrides['sati.provider'] = true;
|
|
198
|
+
if (isEnvVarSet('MORPHEUS_SATI_MODEL'))
|
|
199
|
+
overrides['sati.model'] = true;
|
|
200
|
+
if (isEnvVarSet('MORPHEUS_SATI_TEMPERATURE'))
|
|
201
|
+
overrides['sati.temperature'] = true;
|
|
202
|
+
if (isEnvVarSet('MORPHEUS_SATI_API_KEY'))
|
|
203
|
+
overrides['sati.api_key'] = true;
|
|
204
|
+
// Neo
|
|
205
|
+
if (isEnvVarSet('MORPHEUS_NEO_PROVIDER'))
|
|
206
|
+
overrides['neo.provider'] = true;
|
|
207
|
+
if (isEnvVarSet('MORPHEUS_NEO_MODEL'))
|
|
208
|
+
overrides['neo.model'] = true;
|
|
209
|
+
if (isEnvVarSet('MORPHEUS_NEO_TEMPERATURE'))
|
|
210
|
+
overrides['neo.temperature'] = true;
|
|
211
|
+
if (isEnvVarSet('MORPHEUS_NEO_API_KEY'))
|
|
212
|
+
overrides['neo.api_key'] = true;
|
|
213
|
+
// Apoc
|
|
214
|
+
if (isEnvVarSet('MORPHEUS_APOC_PROVIDER'))
|
|
215
|
+
overrides['apoc.provider'] = true;
|
|
216
|
+
if (isEnvVarSet('MORPHEUS_APOC_MODEL'))
|
|
217
|
+
overrides['apoc.model'] = true;
|
|
218
|
+
if (isEnvVarSet('MORPHEUS_APOC_TEMPERATURE'))
|
|
219
|
+
overrides['apoc.temperature'] = true;
|
|
220
|
+
if (isEnvVarSet('MORPHEUS_APOC_API_KEY'))
|
|
221
|
+
overrides['apoc.api_key'] = true;
|
|
222
|
+
if (isEnvVarSet('MORPHEUS_APOC_WORKING_DIR'))
|
|
223
|
+
overrides['apoc.working_dir'] = true;
|
|
224
|
+
if (isEnvVarSet('MORPHEUS_APOC_TIMEOUT_MS'))
|
|
225
|
+
overrides['apoc.timeout_ms'] = true;
|
|
226
|
+
// Trinity
|
|
227
|
+
if (isEnvVarSet('MORPHEUS_TRINITY_PROVIDER'))
|
|
228
|
+
overrides['trinity.provider'] = true;
|
|
229
|
+
if (isEnvVarSet('MORPHEUS_TRINITY_MODEL'))
|
|
230
|
+
overrides['trinity.model'] = true;
|
|
231
|
+
if (isEnvVarSet('MORPHEUS_TRINITY_TEMPERATURE'))
|
|
232
|
+
overrides['trinity.temperature'] = true;
|
|
233
|
+
if (isEnvVarSet('MORPHEUS_TRINITY_API_KEY'))
|
|
234
|
+
overrides['trinity.api_key'] = true;
|
|
235
|
+
// Audio
|
|
236
|
+
if (isEnvVarSet('MORPHEUS_AUDIO_PROVIDER'))
|
|
237
|
+
overrides['audio.provider'] = true;
|
|
238
|
+
if (isEnvVarSet('MORPHEUS_AUDIO_MODEL'))
|
|
239
|
+
overrides['audio.model'] = true;
|
|
240
|
+
if (isEnvVarSet('MORPHEUS_AUDIO_API_KEY'))
|
|
241
|
+
overrides['audio.apiKey'] = true;
|
|
242
|
+
if (isEnvVarSet('MORPHEUS_AUDIO_MAX_DURATION'))
|
|
243
|
+
overrides['audio.maxDurationSeconds'] = true;
|
|
244
|
+
return overrides;
|
|
245
|
+
}
|
package/dist/http/api.js
CHANGED
|
@@ -18,6 +18,7 @@ import { Trinity } from '../runtime/trinity.js';
|
|
|
18
18
|
import { ChronosRepository } from '../runtime/chronos/repository.js';
|
|
19
19
|
import { ChronosWorker } from '../runtime/chronos/worker.js';
|
|
20
20
|
import { createChronosJobRouter, createChronosConfigRouter } from './routers/chronos.js';
|
|
21
|
+
import { getActiveEnvOverrides } from '../config/precedence.js';
|
|
21
22
|
async function readLastLines(filePath, n) {
|
|
22
23
|
try {
|
|
23
24
|
const content = await fs.readFile(filePath, 'utf8');
|
|
@@ -813,6 +814,34 @@ export function createApiRouter(oracle, chronosWorker) {
|
|
|
813
814
|
res.status(500).json({ error: error.message });
|
|
814
815
|
}
|
|
815
816
|
});
|
|
817
|
+
// ─── Encryption Status ─────────────────────────────────────────────────────
|
|
818
|
+
router.get('/config/encryption-status', (req, res) => {
|
|
819
|
+
try {
|
|
820
|
+
const status = configManager.getEncryptionStatus();
|
|
821
|
+
res.json(status);
|
|
822
|
+
}
|
|
823
|
+
catch (error) {
|
|
824
|
+
res.status(500).json({ error: error.message });
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
// ─── Environment Variable Overrides ────────────────────────────────────────
|
|
828
|
+
router.get('/config/env-overrides', (req, res) => {
|
|
829
|
+
try {
|
|
830
|
+
const overrides = getActiveEnvOverrides();
|
|
831
|
+
// Debug log to see what's being detected
|
|
832
|
+
// console.log('[DEBUG] Env overrides:', JSON.stringify(overrides, null, 2));
|
|
833
|
+
// console.log('[DEBUG] Sample env vars:', {
|
|
834
|
+
// MORPHEUS_LLM_PROVIDER: !!process.env.MORPHEUS_LLM_PROVIDER,
|
|
835
|
+
// MORPHEUS_LLM_MODEL: !!process.env.MORPHEUS_LLM_MODEL,
|
|
836
|
+
// OPENAI_API_KEY: !!process.env.OPENAI_API_KEY,
|
|
837
|
+
// });
|
|
838
|
+
res.json(overrides);
|
|
839
|
+
}
|
|
840
|
+
catch (error) {
|
|
841
|
+
console.error('[ERROR] Failed to get env overrides:', error);
|
|
842
|
+
res.status(500).json({ error: error.message });
|
|
843
|
+
}
|
|
844
|
+
});
|
|
816
845
|
// ─── Trinity Databases CRUD ─────────────────────────────────────────────────
|
|
817
846
|
const DatabaseCreateSchema = z.object({
|
|
818
847
|
name: z.string().min(1).max(100),
|
|
@@ -5,6 +5,7 @@ import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
|
|
|
5
5
|
import { ProviderError } from "../errors.js";
|
|
6
6
|
import { createAgent, createMiddleware } from "langchain";
|
|
7
7
|
import { DisplayManager } from "../display.js";
|
|
8
|
+
import { getUsableApiKey } from "../trinity-crypto.js";
|
|
8
9
|
export class ProviderFactory {
|
|
9
10
|
static buildMonitoringMiddleware() {
|
|
10
11
|
const display = DisplayManager.getInstance();
|
|
@@ -26,24 +27,25 @@ export class ProviderFactory {
|
|
|
26
27
|
});
|
|
27
28
|
}
|
|
28
29
|
static buildModel(config) {
|
|
30
|
+
const usableApiKey = getUsableApiKey(config.api_key);
|
|
29
31
|
switch (config.provider) {
|
|
30
32
|
case 'openai':
|
|
31
33
|
return new ChatOpenAI({
|
|
32
34
|
modelName: config.model,
|
|
33
35
|
temperature: config.temperature,
|
|
34
|
-
apiKey: process.env.OPENAI_API_KEY ||
|
|
36
|
+
apiKey: process.env.OPENAI_API_KEY || usableApiKey,
|
|
35
37
|
});
|
|
36
38
|
case 'anthropic':
|
|
37
39
|
return new ChatAnthropic({
|
|
38
40
|
modelName: config.model,
|
|
39
41
|
temperature: config.temperature,
|
|
40
|
-
apiKey: process.env.ANTHROPIC_API_KEY ||
|
|
42
|
+
apiKey: process.env.ANTHROPIC_API_KEY || usableApiKey,
|
|
41
43
|
});
|
|
42
44
|
case 'openrouter':
|
|
43
45
|
return new ChatOpenAI({
|
|
44
46
|
modelName: config.model,
|
|
45
47
|
temperature: config.temperature,
|
|
46
|
-
apiKey: process.env.OPENROUTER_API_KEY ||
|
|
48
|
+
apiKey: process.env.OPENROUTER_API_KEY || usableApiKey,
|
|
47
49
|
configuration: {
|
|
48
50
|
baseURL: config.base_url || 'https://openrouter.ai/api/v1'
|
|
49
51
|
}
|
|
@@ -52,13 +54,13 @@ export class ProviderFactory {
|
|
|
52
54
|
return new ChatOllama({
|
|
53
55
|
model: config.model,
|
|
54
56
|
temperature: config.temperature,
|
|
55
|
-
baseUrl: config.base_url ||
|
|
57
|
+
baseUrl: config.base_url || usableApiKey,
|
|
56
58
|
});
|
|
57
59
|
case 'gemini':
|
|
58
60
|
return new ChatGoogleGenerativeAI({
|
|
59
61
|
model: config.model,
|
|
60
62
|
temperature: config.temperature,
|
|
61
|
-
apiKey: process.env.GOOGLE_API_KEY ||
|
|
63
|
+
apiKey: process.env.GOOGLE_API_KEY || usableApiKey
|
|
62
64
|
});
|
|
63
65
|
default:
|
|
64
66
|
throw new Error(`Unsupported provider: ${config.provider}`);
|
|
@@ -50,3 +50,54 @@ export function decrypt(ciphertext) {
|
|
|
50
50
|
export function canEncrypt() {
|
|
51
51
|
return !!process.env.MORPHEUS_SECRET;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Checks if a string appears to be an encrypted value.
|
|
55
|
+
* Format: base64(iv):base64(authTag):base64(ciphertext)
|
|
56
|
+
*/
|
|
57
|
+
export function looksLikeEncrypted(value) {
|
|
58
|
+
const parts = value.split(':');
|
|
59
|
+
if (parts.length !== 3)
|
|
60
|
+
return false;
|
|
61
|
+
// Each part should be valid base64
|
|
62
|
+
return parts.every(part => {
|
|
63
|
+
try {
|
|
64
|
+
const decoded = Buffer.from(part, 'base64');
|
|
65
|
+
return decoded.toString('base64') === part;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Safely decrypts a value, returning null if decryption fails.
|
|
74
|
+
* Use this for fail-open scenarios where MORPHEUS_SECRET may not be set.
|
|
75
|
+
*/
|
|
76
|
+
export function safeDecrypt(ciphertext) {
|
|
77
|
+
if (!ciphertext)
|
|
78
|
+
return null;
|
|
79
|
+
try {
|
|
80
|
+
return decrypt(ciphertext);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Gets the usable API key from a potentially encrypted value.
|
|
88
|
+
* If the value looks encrypted, attempts to decrypt it.
|
|
89
|
+
* If decryption fails or MORPHEUS_SECRET is not set, returns the original value.
|
|
90
|
+
* Use this when you need the actual plaintext key for API calls.
|
|
91
|
+
*/
|
|
92
|
+
export function getUsableApiKey(encryptedOrPlain) {
|
|
93
|
+
if (!encryptedOrPlain)
|
|
94
|
+
return undefined;
|
|
95
|
+
// If it looks encrypted, try to decrypt
|
|
96
|
+
if (looksLikeEncrypted(encryptedOrPlain)) {
|
|
97
|
+
const decrypted = safeDecrypt(encryptedOrPlain);
|
|
98
|
+
// If decryption succeeded, return it; otherwise return original
|
|
99
|
+
return decrypted ?? encryptedOrPlain;
|
|
100
|
+
}
|
|
101
|
+
// Not encrypted, return as-is
|
|
102
|
+
return encryptedOrPlain;
|
|
103
|
+
}
|