@elizaos/plugin-secrets-manager 1.0.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/index.d.ts +1461 -0
- package/dist/index.js +4171 -0
- package/dist/index.js.map +1 -0
- package/package.json +82 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4171 @@
|
|
|
1
|
+
// src/plugin.ts
|
|
2
|
+
import { logger as logger13 } from "@elizaos/core";
|
|
3
|
+
|
|
4
|
+
// src/services/secrets.ts
|
|
5
|
+
import {
|
|
6
|
+
Service,
|
|
7
|
+
logger as logger5
|
|
8
|
+
} from "@elizaos/core";
|
|
9
|
+
|
|
10
|
+
// src/types.ts
|
|
11
|
+
var SECRET_KEY_MAX_LENGTH = 256;
|
|
12
|
+
var SECRET_VALUE_MAX_LENGTH = 65536;
|
|
13
|
+
var SECRET_DESCRIPTION_MAX_LENGTH = 1024;
|
|
14
|
+
var SECRET_KEY_PATTERN = /^[A-Z][A-Z0-9_]*$/;
|
|
15
|
+
var MAX_ACCESS_LOG_ENTRIES = 1e3;
|
|
16
|
+
var SecretsError = class extends Error {
|
|
17
|
+
constructor(message, code, details) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.details = details;
|
|
21
|
+
this.name = "SecretsError";
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var PermissionDeniedError = class extends SecretsError {
|
|
25
|
+
constructor(key, action, context) {
|
|
26
|
+
super(
|
|
27
|
+
`Permission denied: cannot ${action} secret '${key}' at level '${context.level}'`,
|
|
28
|
+
"PERMISSION_DENIED",
|
|
29
|
+
{ key, action, context }
|
|
30
|
+
);
|
|
31
|
+
this.name = "PermissionDeniedError";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var SecretNotFoundError = class extends SecretsError {
|
|
35
|
+
constructor(key, context) {
|
|
36
|
+
super(
|
|
37
|
+
`Secret '${key}' not found at level '${context.level}'`,
|
|
38
|
+
"SECRET_NOT_FOUND",
|
|
39
|
+
{ key, context }
|
|
40
|
+
);
|
|
41
|
+
this.name = "SecretNotFoundError";
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var ValidationError = class extends SecretsError {
|
|
45
|
+
constructor(key, message, details) {
|
|
46
|
+
super(
|
|
47
|
+
`Validation failed for secret '${key}': ${message}`,
|
|
48
|
+
"VALIDATION_FAILED",
|
|
49
|
+
{ key, ...details }
|
|
50
|
+
);
|
|
51
|
+
this.name = "ValidationError";
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var EncryptionError = class extends SecretsError {
|
|
55
|
+
constructor(message, details) {
|
|
56
|
+
super(message, "ENCRYPTION_ERROR", details);
|
|
57
|
+
this.name = "EncryptionError";
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
var StorageError = class extends SecretsError {
|
|
61
|
+
constructor(message, details) {
|
|
62
|
+
super(message, "STORAGE_ERROR", details);
|
|
63
|
+
this.name = "StorageError";
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// src/crypto/encryption.ts
|
|
68
|
+
import {
|
|
69
|
+
createCipheriv,
|
|
70
|
+
createDecipheriv,
|
|
71
|
+
randomBytes,
|
|
72
|
+
createHash,
|
|
73
|
+
pbkdf2Sync,
|
|
74
|
+
scryptSync
|
|
75
|
+
} from "crypto";
|
|
76
|
+
var ALGORITHM_GCM = "aes-256-gcm";
|
|
77
|
+
var ALGORITHM_CBC = "aes-256-cbc";
|
|
78
|
+
var IV_LENGTH = 16;
|
|
79
|
+
var KEY_LENGTH = 32;
|
|
80
|
+
var DEFAULT_SALT_LENGTH = 32;
|
|
81
|
+
var DEFAULT_PBKDF2_ITERATIONS = 1e5;
|
|
82
|
+
function generateSalt(length = DEFAULT_SALT_LENGTH) {
|
|
83
|
+
return randomBytes(length).toString("base64");
|
|
84
|
+
}
|
|
85
|
+
function generateKey() {
|
|
86
|
+
return randomBytes(KEY_LENGTH);
|
|
87
|
+
}
|
|
88
|
+
function deriveKeyPbkdf2(password, salt, iterations = DEFAULT_PBKDF2_ITERATIONS) {
|
|
89
|
+
const saltBuffer = typeof salt === "string" ? Buffer.from(salt, "base64") : salt;
|
|
90
|
+
return pbkdf2Sync(password, saltBuffer, iterations, KEY_LENGTH, "sha256");
|
|
91
|
+
}
|
|
92
|
+
function deriveKeyFromAgentId(agentId, salt = "default-salt") {
|
|
93
|
+
return createHash("sha256").update(agentId + salt).digest();
|
|
94
|
+
}
|
|
95
|
+
function createKeyDerivationParams(salt, iterations = DEFAULT_PBKDF2_ITERATIONS) {
|
|
96
|
+
return {
|
|
97
|
+
salt: salt ?? generateSalt(),
|
|
98
|
+
iterations,
|
|
99
|
+
algorithm: "pbkdf2-sha256",
|
|
100
|
+
keyLength: KEY_LENGTH
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function encryptGcm(plaintext, key, keyId = "default") {
|
|
104
|
+
if (key.length !== KEY_LENGTH) {
|
|
105
|
+
throw new EncryptionError(
|
|
106
|
+
`Invalid key length: expected ${KEY_LENGTH}, got ${key.length}`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const iv = randomBytes(IV_LENGTH);
|
|
110
|
+
const cipher = createCipheriv(ALGORITHM_GCM, key, iv);
|
|
111
|
+
let encrypted = cipher.update(plaintext, "utf8", "base64");
|
|
112
|
+
encrypted += cipher.final("base64");
|
|
113
|
+
const authTag = cipher.getAuthTag();
|
|
114
|
+
return {
|
|
115
|
+
value: encrypted,
|
|
116
|
+
iv: iv.toString("base64"),
|
|
117
|
+
authTag: authTag.toString("base64"),
|
|
118
|
+
algorithm: "aes-256-gcm",
|
|
119
|
+
keyId
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function encrypt(plaintext, key, keyId = "default") {
|
|
123
|
+
return encryptGcm(plaintext, key, keyId);
|
|
124
|
+
}
|
|
125
|
+
function decryptGcm(encrypted, key) {
|
|
126
|
+
if (key.length !== KEY_LENGTH) {
|
|
127
|
+
throw new EncryptionError(
|
|
128
|
+
`Invalid key length: expected ${KEY_LENGTH}, got ${key.length}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (encrypted.algorithm !== "aes-256-gcm") {
|
|
132
|
+
throw new EncryptionError(
|
|
133
|
+
`Algorithm mismatch: expected aes-256-gcm, got ${encrypted.algorithm}`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (!encrypted.authTag) {
|
|
137
|
+
throw new EncryptionError("Missing authentication tag for GCM decryption");
|
|
138
|
+
}
|
|
139
|
+
const iv = Buffer.from(encrypted.iv, "base64");
|
|
140
|
+
const authTag = Buffer.from(encrypted.authTag, "base64");
|
|
141
|
+
const decipher = createDecipheriv(ALGORITHM_GCM, key, iv);
|
|
142
|
+
decipher.setAuthTag(authTag);
|
|
143
|
+
let decrypted = decipher.update(encrypted.value, "base64", "utf8");
|
|
144
|
+
decrypted += decipher.final("utf8");
|
|
145
|
+
return decrypted;
|
|
146
|
+
}
|
|
147
|
+
function decryptCbc(encrypted, key) {
|
|
148
|
+
if (key.length !== KEY_LENGTH) {
|
|
149
|
+
throw new EncryptionError(
|
|
150
|
+
`Invalid key length: expected ${KEY_LENGTH}, got ${key.length}`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (encrypted.algorithm !== "aes-256-cbc") {
|
|
154
|
+
throw new EncryptionError(
|
|
155
|
+
`Algorithm mismatch: expected aes-256-cbc, got ${encrypted.algorithm}`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
const iv = Buffer.from(encrypted.iv, "base64");
|
|
159
|
+
const decipher = createDecipheriv(ALGORITHM_CBC, key, iv);
|
|
160
|
+
let decrypted = decipher.update(encrypted.value, "base64", "utf8");
|
|
161
|
+
decrypted += decipher.final("utf8");
|
|
162
|
+
return decrypted;
|
|
163
|
+
}
|
|
164
|
+
function decrypt(encrypted, key) {
|
|
165
|
+
if (typeof encrypted === "string") {
|
|
166
|
+
return encrypted;
|
|
167
|
+
}
|
|
168
|
+
switch (encrypted.algorithm) {
|
|
169
|
+
case "aes-256-gcm":
|
|
170
|
+
return decryptGcm(encrypted, key);
|
|
171
|
+
case "aes-256-cbc":
|
|
172
|
+
return decryptCbc(encrypted, key);
|
|
173
|
+
default:
|
|
174
|
+
throw new EncryptionError(
|
|
175
|
+
`Unsupported algorithm: ${encrypted.algorithm}`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function isEncryptedSecret(value) {
|
|
180
|
+
if (!value || typeof value !== "object") {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
const obj = value;
|
|
184
|
+
return typeof obj.value === "string" && typeof obj.iv === "string" && typeof obj.algorithm === "string" && (obj.algorithm === "aes-256-gcm" || obj.algorithm === "aes-256-cbc");
|
|
185
|
+
}
|
|
186
|
+
function generateSecureToken(length = 32) {
|
|
187
|
+
return randomBytes(length).toString("hex");
|
|
188
|
+
}
|
|
189
|
+
var KeyManager = class {
|
|
190
|
+
keys = /* @__PURE__ */ new Map();
|
|
191
|
+
currentKeyId = "default";
|
|
192
|
+
derivationParams = null;
|
|
193
|
+
constructor(options) {
|
|
194
|
+
if (options?.primaryKey) {
|
|
195
|
+
const keyId = options.primaryKeyId ?? "default";
|
|
196
|
+
this.keys.set(keyId, options.primaryKey);
|
|
197
|
+
this.currentKeyId = keyId;
|
|
198
|
+
}
|
|
199
|
+
if (options?.derivationParams) {
|
|
200
|
+
this.derivationParams = options.derivationParams;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Initialize with a password-derived key
|
|
205
|
+
*/
|
|
206
|
+
initializeFromPassword(password, salt) {
|
|
207
|
+
this.derivationParams = createKeyDerivationParams(salt);
|
|
208
|
+
const key = deriveKeyPbkdf2(
|
|
209
|
+
password,
|
|
210
|
+
this.derivationParams.salt,
|
|
211
|
+
this.derivationParams.iterations
|
|
212
|
+
);
|
|
213
|
+
this.keys.set("default", key);
|
|
214
|
+
this.currentKeyId = "default";
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Initialize with an agent ID (OpenClaw compatible)
|
|
218
|
+
*/
|
|
219
|
+
initializeFromAgentId(agentId, salt) {
|
|
220
|
+
const key = deriveKeyFromAgentId(agentId, salt);
|
|
221
|
+
this.keys.set("default", key);
|
|
222
|
+
this.currentKeyId = "default";
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Add a key for decryption (supports key rotation)
|
|
226
|
+
*/
|
|
227
|
+
addKey(keyId, key) {
|
|
228
|
+
this.keys.set(keyId, key);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Set the current key for encryption
|
|
232
|
+
*/
|
|
233
|
+
setCurrentKey(keyId) {
|
|
234
|
+
if (!this.keys.has(keyId)) {
|
|
235
|
+
throw new EncryptionError(`Key not found: ${keyId}`);
|
|
236
|
+
}
|
|
237
|
+
this.currentKeyId = keyId;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Get the current key ID
|
|
241
|
+
*/
|
|
242
|
+
getCurrentKeyId() {
|
|
243
|
+
return this.currentKeyId;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Get a key by ID
|
|
247
|
+
*/
|
|
248
|
+
getKey(keyId) {
|
|
249
|
+
return this.keys.get(keyId);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Get the current encryption key
|
|
253
|
+
*/
|
|
254
|
+
getCurrentKey() {
|
|
255
|
+
const key = this.keys.get(this.currentKeyId);
|
|
256
|
+
if (!key) {
|
|
257
|
+
throw new EncryptionError("No encryption key configured");
|
|
258
|
+
}
|
|
259
|
+
return key;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Get derivation parameters (for storage)
|
|
263
|
+
*/
|
|
264
|
+
getDerivationParams() {
|
|
265
|
+
return this.derivationParams;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Encrypt a value with the current key
|
|
269
|
+
*/
|
|
270
|
+
encrypt(plaintext) {
|
|
271
|
+
return encryptGcm(plaintext, this.getCurrentKey(), this.currentKeyId);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Decrypt a value (automatically selects the correct key)
|
|
275
|
+
*/
|
|
276
|
+
decrypt(encrypted) {
|
|
277
|
+
if (typeof encrypted === "string") {
|
|
278
|
+
return encrypted;
|
|
279
|
+
}
|
|
280
|
+
const key = this.keys.get(encrypted.keyId);
|
|
281
|
+
if (!key) {
|
|
282
|
+
throw new EncryptionError(
|
|
283
|
+
`Key not found for decryption: ${encrypted.keyId}`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
return decrypt(encrypted, key);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Re-encrypt a value with the current key (for key rotation)
|
|
290
|
+
*/
|
|
291
|
+
reencrypt(encrypted) {
|
|
292
|
+
const plaintext = this.decrypt(encrypted);
|
|
293
|
+
return this.encrypt(plaintext);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Clear all keys from memory
|
|
297
|
+
*/
|
|
298
|
+
clear() {
|
|
299
|
+
for (const key of this.keys.values()) {
|
|
300
|
+
key.fill(0);
|
|
301
|
+
}
|
|
302
|
+
this.keys.clear();
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// src/storage/interface.ts
|
|
307
|
+
var BaseSecretStorage = class {
|
|
308
|
+
/**
|
|
309
|
+
* Create a default secret configuration
|
|
310
|
+
*/
|
|
311
|
+
createDefaultConfig(key, context, partial) {
|
|
312
|
+
return {
|
|
313
|
+
type: partial?.type ?? "secret",
|
|
314
|
+
required: partial?.required ?? false,
|
|
315
|
+
description: partial?.description ?? `Secret: ${key}`,
|
|
316
|
+
canGenerate: partial?.canGenerate ?? false,
|
|
317
|
+
validationMethod: partial?.validationMethod,
|
|
318
|
+
status: partial?.status ?? "valid",
|
|
319
|
+
lastError: partial?.lastError,
|
|
320
|
+
attempts: partial?.attempts ?? 0,
|
|
321
|
+
createdAt: partial?.createdAt ?? Date.now(),
|
|
322
|
+
validatedAt: partial?.validatedAt ?? Date.now(),
|
|
323
|
+
plugin: partial?.plugin ?? context.level,
|
|
324
|
+
level: context.level,
|
|
325
|
+
ownerId: context.userId,
|
|
326
|
+
worldId: context.worldId,
|
|
327
|
+
encrypted: partial?.encrypted ?? true,
|
|
328
|
+
permissions: partial?.permissions ?? [],
|
|
329
|
+
sharedWith: partial?.sharedWith ?? [],
|
|
330
|
+
expiresAt: partial?.expiresAt
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
var CompositeSecretStorage = class {
|
|
335
|
+
storageType = "memory";
|
|
336
|
+
globalStorage;
|
|
337
|
+
worldStorage;
|
|
338
|
+
userStorage;
|
|
339
|
+
constructor(options) {
|
|
340
|
+
this.globalStorage = options.globalStorage;
|
|
341
|
+
this.worldStorage = options.worldStorage;
|
|
342
|
+
this.userStorage = options.userStorage;
|
|
343
|
+
}
|
|
344
|
+
async initialize() {
|
|
345
|
+
await Promise.all([
|
|
346
|
+
this.globalStorage.initialize(),
|
|
347
|
+
this.worldStorage.initialize(),
|
|
348
|
+
this.userStorage.initialize()
|
|
349
|
+
]);
|
|
350
|
+
}
|
|
351
|
+
getStorageForContext(context) {
|
|
352
|
+
switch (context.level) {
|
|
353
|
+
case "global":
|
|
354
|
+
return this.globalStorage;
|
|
355
|
+
case "world":
|
|
356
|
+
return this.worldStorage;
|
|
357
|
+
case "user":
|
|
358
|
+
return this.userStorage;
|
|
359
|
+
default:
|
|
360
|
+
return this.globalStorage;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async exists(key, context) {
|
|
364
|
+
return this.getStorageForContext(context).exists(key, context);
|
|
365
|
+
}
|
|
366
|
+
async get(key, context) {
|
|
367
|
+
return this.getStorageForContext(context).get(key, context);
|
|
368
|
+
}
|
|
369
|
+
async set(key, value, context, config) {
|
|
370
|
+
return this.getStorageForContext(context).set(key, value, context, config);
|
|
371
|
+
}
|
|
372
|
+
async delete(key, context) {
|
|
373
|
+
return this.getStorageForContext(context).delete(key, context);
|
|
374
|
+
}
|
|
375
|
+
async list(context) {
|
|
376
|
+
return this.getStorageForContext(context).list(context);
|
|
377
|
+
}
|
|
378
|
+
async getConfig(key, context) {
|
|
379
|
+
return this.getStorageForContext(context).getConfig(key, context);
|
|
380
|
+
}
|
|
381
|
+
async updateConfig(key, context, config) {
|
|
382
|
+
return this.getStorageForContext(context).updateConfig(
|
|
383
|
+
key,
|
|
384
|
+
context,
|
|
385
|
+
config
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// src/storage/memory-store.ts
|
|
391
|
+
var MemorySecretStorage = class extends BaseSecretStorage {
|
|
392
|
+
storageType = "memory";
|
|
393
|
+
store = /* @__PURE__ */ new Map();
|
|
394
|
+
async initialize() {
|
|
395
|
+
}
|
|
396
|
+
async exists(key, context) {
|
|
397
|
+
const storageKey = this.generateStorageKey(key, context);
|
|
398
|
+
return this.store.has(storageKey);
|
|
399
|
+
}
|
|
400
|
+
async get(key, context) {
|
|
401
|
+
const storageKey = this.generateStorageKey(key, context);
|
|
402
|
+
const entry = this.store.get(storageKey);
|
|
403
|
+
if (!entry) {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
if (entry.config.expiresAt && entry.config.expiresAt < Date.now()) {
|
|
407
|
+
this.store.delete(storageKey);
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
return entry.value;
|
|
411
|
+
}
|
|
412
|
+
async set(key, value, context, config) {
|
|
413
|
+
const storageKey = this.generateStorageKey(key, context);
|
|
414
|
+
const existingEntry = this.store.get(storageKey);
|
|
415
|
+
const fullConfig = this.createDefaultConfig(key, context, {
|
|
416
|
+
...existingEntry?.config,
|
|
417
|
+
...config
|
|
418
|
+
});
|
|
419
|
+
this.store.set(storageKey, {
|
|
420
|
+
value,
|
|
421
|
+
config: fullConfig
|
|
422
|
+
});
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
async delete(key, context) {
|
|
426
|
+
const storageKey = this.generateStorageKey(key, context);
|
|
427
|
+
return this.store.delete(storageKey);
|
|
428
|
+
}
|
|
429
|
+
async list(context) {
|
|
430
|
+
const prefix = this.getContextPrefix(context);
|
|
431
|
+
const metadata = {};
|
|
432
|
+
for (const [storageKey, entry] of this.store) {
|
|
433
|
+
if (storageKey.startsWith(prefix)) {
|
|
434
|
+
const originalKey = this.extractOriginalKey(storageKey, context);
|
|
435
|
+
if (originalKey) {
|
|
436
|
+
if (entry.config.expiresAt && entry.config.expiresAt < Date.now()) {
|
|
437
|
+
this.store.delete(storageKey);
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
metadata[originalKey] = { ...entry.config };
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return metadata;
|
|
445
|
+
}
|
|
446
|
+
async getConfig(key, context) {
|
|
447
|
+
const storageKey = this.generateStorageKey(key, context);
|
|
448
|
+
const entry = this.store.get(storageKey);
|
|
449
|
+
if (!entry) {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
return { ...entry.config };
|
|
453
|
+
}
|
|
454
|
+
async updateConfig(key, context, config) {
|
|
455
|
+
const storageKey = this.generateStorageKey(key, context);
|
|
456
|
+
const entry = this.store.get(storageKey);
|
|
457
|
+
if (!entry) {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
entry.config = {
|
|
461
|
+
...entry.config,
|
|
462
|
+
...config
|
|
463
|
+
};
|
|
464
|
+
this.store.set(storageKey, entry);
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Generate a storage key from the secret key and context
|
|
469
|
+
*/
|
|
470
|
+
generateStorageKey(key, context) {
|
|
471
|
+
switch (context.level) {
|
|
472
|
+
case "global":
|
|
473
|
+
return `global:${context.agentId}:${key}`;
|
|
474
|
+
case "world":
|
|
475
|
+
return `world:${context.worldId}:${key}`;
|
|
476
|
+
case "user":
|
|
477
|
+
return `user:${context.userId}:${key}`;
|
|
478
|
+
default:
|
|
479
|
+
return `unknown:${key}`;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Get the storage key prefix for a context level
|
|
484
|
+
*/
|
|
485
|
+
getContextPrefix(context) {
|
|
486
|
+
switch (context.level) {
|
|
487
|
+
case "global":
|
|
488
|
+
return `global:${context.agentId}:`;
|
|
489
|
+
case "world":
|
|
490
|
+
return `world:${context.worldId}:`;
|
|
491
|
+
case "user":
|
|
492
|
+
return `user:${context.userId}:`;
|
|
493
|
+
default:
|
|
494
|
+
return "";
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Extract the original key from a storage key
|
|
499
|
+
*/
|
|
500
|
+
extractOriginalKey(storageKey, context) {
|
|
501
|
+
const prefix = this.getContextPrefix(context);
|
|
502
|
+
if (!storageKey.startsWith(prefix)) {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
return storageKey.slice(prefix.length);
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Clear all stored secrets (for testing)
|
|
509
|
+
*/
|
|
510
|
+
clear() {
|
|
511
|
+
this.store.clear();
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Get the number of stored secrets (for testing)
|
|
515
|
+
*/
|
|
516
|
+
size() {
|
|
517
|
+
return this.store.size;
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
// src/storage/character-store.ts
|
|
522
|
+
import { logger } from "@elizaos/core";
|
|
523
|
+
var SECRETS_KEY = "secrets";
|
|
524
|
+
var METADATA_KEY = "__secrets_metadata";
|
|
525
|
+
var CharacterSettingsStorage = class extends BaseSecretStorage {
|
|
526
|
+
storageType = "character";
|
|
527
|
+
runtime;
|
|
528
|
+
keyManager;
|
|
529
|
+
initialized = false;
|
|
530
|
+
constructor(runtime, keyManager) {
|
|
531
|
+
super();
|
|
532
|
+
this.runtime = runtime;
|
|
533
|
+
this.keyManager = keyManager;
|
|
534
|
+
}
|
|
535
|
+
async initialize() {
|
|
536
|
+
if (this.initialized) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
logger.debug("[CharacterSettingsStorage] Initializing");
|
|
540
|
+
this.ensureSettingsStructure();
|
|
541
|
+
this.initialized = true;
|
|
542
|
+
logger.debug("[CharacterSettingsStorage] Initialized");
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Ensure the character.settings.secrets structure exists
|
|
546
|
+
*/
|
|
547
|
+
ensureSettingsStructure() {
|
|
548
|
+
if (!this.runtime.character.settings) {
|
|
549
|
+
this.runtime.character.settings = {};
|
|
550
|
+
}
|
|
551
|
+
if (!this.runtime.character.settings[SECRETS_KEY]) {
|
|
552
|
+
this.runtime.character.settings[SECRETS_KEY] = {};
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
async exists(key, _context) {
|
|
556
|
+
this.ensureSettingsStructure();
|
|
557
|
+
const secrets = this.getSecretsObject();
|
|
558
|
+
return key in secrets;
|
|
559
|
+
}
|
|
560
|
+
async get(key, context) {
|
|
561
|
+
this.ensureSettingsStructure();
|
|
562
|
+
const secrets = this.getSecretsObject();
|
|
563
|
+
const stored = secrets[key];
|
|
564
|
+
if (stored === void 0 || stored === null) {
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
if (typeof stored === "string") {
|
|
568
|
+
return stored;
|
|
569
|
+
}
|
|
570
|
+
if (typeof stored === "object") {
|
|
571
|
+
const storedSecret = stored;
|
|
572
|
+
if (storedSecret.config?.expiresAt && storedSecret.config.expiresAt < Date.now()) {
|
|
573
|
+
await this.delete(key, context);
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
if (isEncryptedSecret(storedSecret.value)) {
|
|
577
|
+
return this.keyManager.decrypt(storedSecret.value);
|
|
578
|
+
}
|
|
579
|
+
if (typeof storedSecret.value === "string") {
|
|
580
|
+
return storedSecret.value;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
async set(key, value, context, config) {
|
|
586
|
+
this.ensureSettingsStructure();
|
|
587
|
+
const secrets = this.getSecretsObject();
|
|
588
|
+
const existingStored = secrets[key];
|
|
589
|
+
const existingConfig = typeof existingStored === "object" ? existingStored.config : void 0;
|
|
590
|
+
const fullConfig = this.createDefaultConfig(key, context, {
|
|
591
|
+
...existingConfig,
|
|
592
|
+
...config
|
|
593
|
+
});
|
|
594
|
+
const shouldEncrypt = fullConfig.encrypted !== false;
|
|
595
|
+
const storedValue = shouldEncrypt ? this.keyManager.encrypt(value) : value;
|
|
596
|
+
const storedSecret = {
|
|
597
|
+
value: storedValue,
|
|
598
|
+
config: fullConfig
|
|
599
|
+
};
|
|
600
|
+
secrets[key] = storedSecret;
|
|
601
|
+
logger.debug(`[CharacterSettingsStorage] Set secret: ${key}`);
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
async delete(key, _context) {
|
|
605
|
+
this.ensureSettingsStructure();
|
|
606
|
+
const secrets = this.getSecretsObject();
|
|
607
|
+
if (!(key in secrets)) {
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
delete secrets[key];
|
|
611
|
+
logger.debug(`[CharacterSettingsStorage] Deleted secret: ${key}`);
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
async list(_context) {
|
|
615
|
+
const secrets = this.getSecretsObject();
|
|
616
|
+
const metadata = {};
|
|
617
|
+
for (const [key, stored] of Object.entries(secrets)) {
|
|
618
|
+
if (key === METADATA_KEY) {
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
if (typeof stored === "object" && stored !== null) {
|
|
622
|
+
const storedSecret = stored;
|
|
623
|
+
if (storedSecret.config?.expiresAt && storedSecret.config.expiresAt < Date.now()) {
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
if (storedSecret.config) {
|
|
627
|
+
metadata[key] = { ...storedSecret.config };
|
|
628
|
+
}
|
|
629
|
+
} else {
|
|
630
|
+
metadata[key] = this.createDefaultConfig(key, {
|
|
631
|
+
level: "global",
|
|
632
|
+
agentId: this.runtime.agentId
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return metadata;
|
|
637
|
+
}
|
|
638
|
+
async getConfig(key, context) {
|
|
639
|
+
const secrets = this.getSecretsObject();
|
|
640
|
+
const stored = secrets[key];
|
|
641
|
+
if (!stored) {
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
if (typeof stored === "object" && "config" in stored) {
|
|
645
|
+
return { ...stored.config };
|
|
646
|
+
}
|
|
647
|
+
return this.createDefaultConfig(key, context);
|
|
648
|
+
}
|
|
649
|
+
async updateConfig(key, _context, config) {
|
|
650
|
+
this.ensureSettingsStructure();
|
|
651
|
+
const secrets = this.getSecretsObject();
|
|
652
|
+
const stored = secrets[key];
|
|
653
|
+
if (!stored) {
|
|
654
|
+
return false;
|
|
655
|
+
}
|
|
656
|
+
if (typeof stored === "object" && "config" in stored) {
|
|
657
|
+
const storedSecret = stored;
|
|
658
|
+
storedSecret.config = {
|
|
659
|
+
...storedSecret.config,
|
|
660
|
+
...config
|
|
661
|
+
};
|
|
662
|
+
secrets[key] = storedSecret;
|
|
663
|
+
} else {
|
|
664
|
+
const defaultConfig = this.createDefaultConfig(key, {
|
|
665
|
+
level: "global",
|
|
666
|
+
agentId: this.runtime.agentId
|
|
667
|
+
});
|
|
668
|
+
const storedSecret = {
|
|
669
|
+
value: stored,
|
|
670
|
+
config: { ...defaultConfig, ...config }
|
|
671
|
+
};
|
|
672
|
+
secrets[key] = storedSecret;
|
|
673
|
+
}
|
|
674
|
+
return true;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Get the secrets object from character settings
|
|
678
|
+
*
|
|
679
|
+
* Accesses character.settings.secrets directly instead of using getSetting()
|
|
680
|
+
* because getSetting() only returns primitives, not objects.
|
|
681
|
+
*/
|
|
682
|
+
getSecretsObject() {
|
|
683
|
+
this.ensureSettingsStructure();
|
|
684
|
+
const settings = this.runtime.character.settings;
|
|
685
|
+
const secrets = settings[SECRETS_KEY];
|
|
686
|
+
if (!secrets || typeof secrets !== "object") {
|
|
687
|
+
return {};
|
|
688
|
+
}
|
|
689
|
+
return secrets;
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Migrate legacy environment variable format to new format
|
|
693
|
+
*/
|
|
694
|
+
async migrateFromEnvVars(envVarPrefix = "ENV_") {
|
|
695
|
+
let migrated = 0;
|
|
696
|
+
const settings = this.runtime.character?.settings ?? {};
|
|
697
|
+
for (const [key, value] of Object.entries(settings)) {
|
|
698
|
+
if (key.startsWith(envVarPrefix) && typeof value === "string") {
|
|
699
|
+
const secretKey = key.slice(envVarPrefix.length);
|
|
700
|
+
const exists = await this.exists(secretKey, {
|
|
701
|
+
level: "global",
|
|
702
|
+
agentId: this.runtime.agentId
|
|
703
|
+
});
|
|
704
|
+
if (!exists) {
|
|
705
|
+
await this.set(
|
|
706
|
+
secretKey,
|
|
707
|
+
value,
|
|
708
|
+
{ level: "global", agentId: this.runtime.agentId },
|
|
709
|
+
{
|
|
710
|
+
description: `Migrated from ${key}`,
|
|
711
|
+
encrypted: true
|
|
712
|
+
}
|
|
713
|
+
);
|
|
714
|
+
migrated++;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
logger.info(
|
|
719
|
+
`[CharacterSettingsStorage] Migrated ${migrated} env vars to secrets`
|
|
720
|
+
);
|
|
721
|
+
return migrated;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Get a secret as an environment variable (for backward compatibility)
|
|
725
|
+
*/
|
|
726
|
+
async getAsEnvVar(key) {
|
|
727
|
+
return this.get(key, { level: "global", agentId: this.runtime.agentId });
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Sync a secret to process.env (for plugins that read env vars directly)
|
|
731
|
+
*/
|
|
732
|
+
async syncToEnv(key, envVarName) {
|
|
733
|
+
const value = await this.getAsEnvVar(key);
|
|
734
|
+
if (value === null) {
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
const envKey = envVarName ?? key;
|
|
738
|
+
process.env[envKey] = value;
|
|
739
|
+
return true;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Sync all secrets to process.env
|
|
743
|
+
*/
|
|
744
|
+
async syncAllToEnv() {
|
|
745
|
+
const metadata = await this.list({
|
|
746
|
+
level: "global",
|
|
747
|
+
agentId: this.runtime.agentId
|
|
748
|
+
});
|
|
749
|
+
let synced = 0;
|
|
750
|
+
for (const key of Object.keys(metadata)) {
|
|
751
|
+
if (await this.syncToEnv(key)) {
|
|
752
|
+
synced++;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return synced;
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
// src/storage/world-store.ts
|
|
760
|
+
import { logger as logger2, Role } from "@elizaos/core";
|
|
761
|
+
var WorldMetadataStorage = class extends BaseSecretStorage {
|
|
762
|
+
storageType = "world";
|
|
763
|
+
runtime;
|
|
764
|
+
keyManager;
|
|
765
|
+
worldCache = /* @__PURE__ */ new Map();
|
|
766
|
+
constructor(runtime, keyManager) {
|
|
767
|
+
super();
|
|
768
|
+
this.runtime = runtime;
|
|
769
|
+
this.keyManager = keyManager;
|
|
770
|
+
}
|
|
771
|
+
async initialize() {
|
|
772
|
+
logger2.debug("[WorldMetadataStorage] Initialized");
|
|
773
|
+
}
|
|
774
|
+
async exists(key, context) {
|
|
775
|
+
if (!context.worldId) {
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
const secrets = await this.getWorldSecrets(context.worldId);
|
|
779
|
+
return key in secrets;
|
|
780
|
+
}
|
|
781
|
+
async get(key, context) {
|
|
782
|
+
if (!context.worldId) {
|
|
783
|
+
logger2.warn("[WorldMetadataStorage] Cannot get secret without worldId");
|
|
784
|
+
return null;
|
|
785
|
+
}
|
|
786
|
+
const secrets = await this.getWorldSecrets(context.worldId);
|
|
787
|
+
const stored = secrets[key];
|
|
788
|
+
if (stored === void 0 || stored === null) {
|
|
789
|
+
return null;
|
|
790
|
+
}
|
|
791
|
+
if (typeof stored === "string") {
|
|
792
|
+
return stored;
|
|
793
|
+
}
|
|
794
|
+
if (typeof stored === "object") {
|
|
795
|
+
const storedSecret = stored;
|
|
796
|
+
if (storedSecret.config?.expiresAt && storedSecret.config.expiresAt < Date.now()) {
|
|
797
|
+
await this.delete(key, context);
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
if (isEncryptedSecret(storedSecret.value)) {
|
|
801
|
+
return this.keyManager.decrypt(storedSecret.value);
|
|
802
|
+
}
|
|
803
|
+
if (typeof storedSecret.value === "string") {
|
|
804
|
+
return storedSecret.value;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
async set(key, value, context, config) {
|
|
810
|
+
if (!context.worldId) {
|
|
811
|
+
throw new StorageError("Cannot set world secret without worldId");
|
|
812
|
+
}
|
|
813
|
+
if (context.requesterId) {
|
|
814
|
+
const hasPermission = await this.checkWritePermission(
|
|
815
|
+
context.worldId,
|
|
816
|
+
context.requesterId
|
|
817
|
+
);
|
|
818
|
+
if (!hasPermission) {
|
|
819
|
+
throw new PermissionDeniedError(key, "write", context);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
const world = await this.getWorld(context.worldId);
|
|
823
|
+
if (!world) {
|
|
824
|
+
throw new StorageError(`World not found: ${context.worldId}`);
|
|
825
|
+
}
|
|
826
|
+
if (!world.metadata) {
|
|
827
|
+
world.metadata = {};
|
|
828
|
+
}
|
|
829
|
+
const worldMeta = world.metadata;
|
|
830
|
+
if (!worldMeta.secrets) {
|
|
831
|
+
worldMeta.secrets = {};
|
|
832
|
+
}
|
|
833
|
+
const secrets = worldMeta.secrets;
|
|
834
|
+
const existingStored = secrets[key];
|
|
835
|
+
const existingConfig = typeof existingStored === "object" ? existingStored.config : void 0;
|
|
836
|
+
const fullConfig = this.createDefaultConfig(key, context, {
|
|
837
|
+
...existingConfig,
|
|
838
|
+
...config,
|
|
839
|
+
worldId: context.worldId
|
|
840
|
+
});
|
|
841
|
+
const shouldEncrypt = fullConfig.encrypted !== false;
|
|
842
|
+
const storedValue = shouldEncrypt ? this.keyManager.encrypt(value) : value;
|
|
843
|
+
const storedSecret = {
|
|
844
|
+
value: storedValue,
|
|
845
|
+
config: fullConfig
|
|
846
|
+
};
|
|
847
|
+
secrets[key] = storedSecret;
|
|
848
|
+
worldMeta.secrets = secrets;
|
|
849
|
+
await this.runtime.updateWorld(world);
|
|
850
|
+
this.worldCache.set(context.worldId, world);
|
|
851
|
+
logger2.debug(
|
|
852
|
+
`[WorldMetadataStorage] Set secret: ${key} for world: ${context.worldId}`
|
|
853
|
+
);
|
|
854
|
+
return true;
|
|
855
|
+
}
|
|
856
|
+
async delete(key, context) {
|
|
857
|
+
if (!context.worldId) {
|
|
858
|
+
return false;
|
|
859
|
+
}
|
|
860
|
+
if (context.requesterId) {
|
|
861
|
+
const hasPermission = await this.checkWritePermission(
|
|
862
|
+
context.worldId,
|
|
863
|
+
context.requesterId
|
|
864
|
+
);
|
|
865
|
+
if (!hasPermission) {
|
|
866
|
+
throw new PermissionDeniedError(key, "delete", context);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
const world = await this.getWorld(context.worldId);
|
|
870
|
+
if (!world) {
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
const metadata = world.metadata;
|
|
874
|
+
if (!metadata?.secrets) {
|
|
875
|
+
return false;
|
|
876
|
+
}
|
|
877
|
+
const secrets = metadata.secrets;
|
|
878
|
+
if (!(key in secrets)) {
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
delete secrets[key];
|
|
882
|
+
metadata.secrets = secrets;
|
|
883
|
+
await this.runtime.updateWorld(world);
|
|
884
|
+
this.worldCache.set(context.worldId, world);
|
|
885
|
+
logger2.debug(
|
|
886
|
+
`[WorldMetadataStorage] Deleted secret: ${key} from world: ${context.worldId}`
|
|
887
|
+
);
|
|
888
|
+
return true;
|
|
889
|
+
}
|
|
890
|
+
async list(context) {
|
|
891
|
+
if (!context.worldId) {
|
|
892
|
+
return {};
|
|
893
|
+
}
|
|
894
|
+
const secrets = await this.getWorldSecrets(context.worldId);
|
|
895
|
+
const metadata = {};
|
|
896
|
+
for (const [key, stored] of Object.entries(secrets)) {
|
|
897
|
+
if (typeof stored === "object" && stored !== null) {
|
|
898
|
+
const storedSecret = stored;
|
|
899
|
+
if (storedSecret.config?.expiresAt && storedSecret.config.expiresAt < Date.now()) {
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
if (storedSecret.config) {
|
|
903
|
+
metadata[key] = { ...storedSecret.config };
|
|
904
|
+
}
|
|
905
|
+
} else {
|
|
906
|
+
metadata[key] = this.createDefaultConfig(key, context);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
return metadata;
|
|
910
|
+
}
|
|
911
|
+
async getConfig(key, context) {
|
|
912
|
+
if (!context.worldId) {
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
const secrets = await this.getWorldSecrets(context.worldId);
|
|
916
|
+
const stored = secrets[key];
|
|
917
|
+
if (!stored) {
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
920
|
+
if (typeof stored === "object" && "config" in stored) {
|
|
921
|
+
return { ...stored.config };
|
|
922
|
+
}
|
|
923
|
+
return this.createDefaultConfig(key, context);
|
|
924
|
+
}
|
|
925
|
+
async updateConfig(key, context, config) {
|
|
926
|
+
if (!context.worldId) {
|
|
927
|
+
return false;
|
|
928
|
+
}
|
|
929
|
+
if (context.requesterId) {
|
|
930
|
+
const hasPermission = await this.checkWritePermission(
|
|
931
|
+
context.worldId,
|
|
932
|
+
context.requesterId
|
|
933
|
+
);
|
|
934
|
+
if (!hasPermission) {
|
|
935
|
+
throw new PermissionDeniedError(key, "write", context);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
const world = await this.getWorld(context.worldId);
|
|
939
|
+
if (!world) {
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
942
|
+
const metadata = world.metadata;
|
|
943
|
+
if (!metadata?.secrets) {
|
|
944
|
+
return false;
|
|
945
|
+
}
|
|
946
|
+
const secrets = metadata.secrets;
|
|
947
|
+
const stored = secrets[key];
|
|
948
|
+
if (!stored) {
|
|
949
|
+
return false;
|
|
950
|
+
}
|
|
951
|
+
if (typeof stored === "object" && "config" in stored) {
|
|
952
|
+
const storedSecret = stored;
|
|
953
|
+
storedSecret.config = {
|
|
954
|
+
...storedSecret.config,
|
|
955
|
+
...config
|
|
956
|
+
};
|
|
957
|
+
secrets[key] = storedSecret;
|
|
958
|
+
} else {
|
|
959
|
+
const defaultConfig = this.createDefaultConfig(key, context);
|
|
960
|
+
const storedSecret = {
|
|
961
|
+
value: stored,
|
|
962
|
+
config: { ...defaultConfig, ...config }
|
|
963
|
+
};
|
|
964
|
+
secrets[key] = storedSecret;
|
|
965
|
+
}
|
|
966
|
+
metadata.secrets = secrets;
|
|
967
|
+
await this.runtime.updateWorld(world);
|
|
968
|
+
this.worldCache.set(context.worldId, world);
|
|
969
|
+
return true;
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Get a world from cache or database
|
|
973
|
+
*/
|
|
974
|
+
async getWorld(worldId) {
|
|
975
|
+
const cached = this.worldCache.get(worldId);
|
|
976
|
+
if (cached) {
|
|
977
|
+
return cached;
|
|
978
|
+
}
|
|
979
|
+
const world = await this.runtime.getWorld(worldId);
|
|
980
|
+
if (world) {
|
|
981
|
+
this.worldCache.set(worldId, world);
|
|
982
|
+
}
|
|
983
|
+
return world;
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Get secrets object from world metadata
|
|
987
|
+
*/
|
|
988
|
+
async getWorldSecrets(worldId) {
|
|
989
|
+
const world = await this.getWorld(worldId);
|
|
990
|
+
const metadata = world?.metadata;
|
|
991
|
+
if (!metadata?.secrets) {
|
|
992
|
+
return {};
|
|
993
|
+
}
|
|
994
|
+
return metadata.secrets;
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Check if a user has write permission in a world
|
|
998
|
+
*/
|
|
999
|
+
async checkWritePermission(worldId, userId) {
|
|
1000
|
+
const world = await this.getWorld(worldId);
|
|
1001
|
+
if (!world) {
|
|
1002
|
+
return false;
|
|
1003
|
+
}
|
|
1004
|
+
if (userId === this.runtime.agentId) {
|
|
1005
|
+
return true;
|
|
1006
|
+
}
|
|
1007
|
+
const roles = world.metadata?.roles;
|
|
1008
|
+
if (!roles) {
|
|
1009
|
+
return false;
|
|
1010
|
+
}
|
|
1011
|
+
const userRole = roles[userId];
|
|
1012
|
+
return userRole === Role.OWNER || userRole === Role.ADMIN;
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Clear the world cache
|
|
1016
|
+
*/
|
|
1017
|
+
clearCache() {
|
|
1018
|
+
this.worldCache.clear();
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Invalidate a specific world in the cache
|
|
1022
|
+
*/
|
|
1023
|
+
invalidateWorld(worldId) {
|
|
1024
|
+
this.worldCache.delete(worldId);
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
// src/storage/component-store.ts
|
|
1029
|
+
import { logger as logger3, createUniqueUuid } from "@elizaos/core";
|
|
1030
|
+
var COMPONENT_TYPE = "secret";
|
|
1031
|
+
var ComponentSecretStorage = class extends BaseSecretStorage {
|
|
1032
|
+
storageType = "component";
|
|
1033
|
+
runtime;
|
|
1034
|
+
keyManager;
|
|
1035
|
+
constructor(runtime, keyManager) {
|
|
1036
|
+
super();
|
|
1037
|
+
this.runtime = runtime;
|
|
1038
|
+
this.keyManager = keyManager;
|
|
1039
|
+
}
|
|
1040
|
+
async initialize() {
|
|
1041
|
+
logger3.debug("[ComponentSecretStorage] Initialized");
|
|
1042
|
+
}
|
|
1043
|
+
async exists(key, context) {
|
|
1044
|
+
if (!context.userId) {
|
|
1045
|
+
return false;
|
|
1046
|
+
}
|
|
1047
|
+
const component = await this.findSecretComponent(context.userId, key);
|
|
1048
|
+
return component !== null;
|
|
1049
|
+
}
|
|
1050
|
+
async get(key, context) {
|
|
1051
|
+
if (!context.userId) {
|
|
1052
|
+
logger3.warn("[ComponentSecretStorage] Cannot get secret without userId");
|
|
1053
|
+
return null;
|
|
1054
|
+
}
|
|
1055
|
+
if (context.requesterId && context.requesterId !== context.userId) {
|
|
1056
|
+
throw new PermissionDeniedError(key, "read", context);
|
|
1057
|
+
}
|
|
1058
|
+
const component = await this.findSecretComponent(context.userId, key);
|
|
1059
|
+
if (!component) {
|
|
1060
|
+
return null;
|
|
1061
|
+
}
|
|
1062
|
+
const data = component.data;
|
|
1063
|
+
if (!data) {
|
|
1064
|
+
return null;
|
|
1065
|
+
}
|
|
1066
|
+
if (data.config?.expiresAt && data.config.expiresAt < Date.now()) {
|
|
1067
|
+
await this.delete(key, context);
|
|
1068
|
+
return null;
|
|
1069
|
+
}
|
|
1070
|
+
if (isEncryptedSecret(data.value)) {
|
|
1071
|
+
return this.keyManager.decrypt(data.value);
|
|
1072
|
+
}
|
|
1073
|
+
if (typeof data.value === "string") {
|
|
1074
|
+
return data.value;
|
|
1075
|
+
}
|
|
1076
|
+
return null;
|
|
1077
|
+
}
|
|
1078
|
+
async set(key, value, context, config) {
|
|
1079
|
+
if (!context.userId) {
|
|
1080
|
+
throw new StorageError("Cannot set user secret without userId");
|
|
1081
|
+
}
|
|
1082
|
+
if (context.requesterId && context.requesterId !== context.userId) {
|
|
1083
|
+
throw new PermissionDeniedError(key, "write", context);
|
|
1084
|
+
}
|
|
1085
|
+
const existingComponent = await this.findSecretComponent(
|
|
1086
|
+
context.userId,
|
|
1087
|
+
key
|
|
1088
|
+
);
|
|
1089
|
+
const existingData = existingComponent?.data;
|
|
1090
|
+
const existingConfig = existingData?.config;
|
|
1091
|
+
const fullConfig = this.createDefaultConfig(key, context, {
|
|
1092
|
+
...existingConfig,
|
|
1093
|
+
...config,
|
|
1094
|
+
ownerId: context.userId
|
|
1095
|
+
});
|
|
1096
|
+
const shouldEncrypt = fullConfig.encrypted !== false;
|
|
1097
|
+
const storedValue = shouldEncrypt ? this.keyManager.encrypt(value) : value;
|
|
1098
|
+
const componentData = {
|
|
1099
|
+
key,
|
|
1100
|
+
value: storedValue,
|
|
1101
|
+
config: fullConfig,
|
|
1102
|
+
updatedAt: Date.now()
|
|
1103
|
+
};
|
|
1104
|
+
if (existingComponent) {
|
|
1105
|
+
await this.runtime.updateComponent({
|
|
1106
|
+
...existingComponent,
|
|
1107
|
+
data: componentData
|
|
1108
|
+
});
|
|
1109
|
+
logger3.debug(
|
|
1110
|
+
`[ComponentSecretStorage] Updated secret: ${key} for user: ${context.userId}`
|
|
1111
|
+
);
|
|
1112
|
+
} else {
|
|
1113
|
+
const newComponent = {
|
|
1114
|
+
id: createUniqueUuid(this.runtime, `${context.userId}-secret-${key}`),
|
|
1115
|
+
createdAt: Date.now(),
|
|
1116
|
+
entityId: context.userId,
|
|
1117
|
+
agentId: this.runtime.agentId,
|
|
1118
|
+
roomId: this.runtime.agentId,
|
|
1119
|
+
worldId: this.runtime.agentId,
|
|
1120
|
+
sourceEntityId: context.userId,
|
|
1121
|
+
type: COMPONENT_TYPE,
|
|
1122
|
+
data: componentData
|
|
1123
|
+
};
|
|
1124
|
+
await this.runtime.createComponent(newComponent);
|
|
1125
|
+
logger3.debug(
|
|
1126
|
+
`[ComponentSecretStorage] Created secret: ${key} for user: ${context.userId}`
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
return true;
|
|
1130
|
+
}
|
|
1131
|
+
async delete(key, context) {
|
|
1132
|
+
if (!context.userId) {
|
|
1133
|
+
return false;
|
|
1134
|
+
}
|
|
1135
|
+
if (context.requesterId && context.requesterId !== context.userId) {
|
|
1136
|
+
throw new PermissionDeniedError(key, "delete", context);
|
|
1137
|
+
}
|
|
1138
|
+
const component = await this.findSecretComponent(context.userId, key);
|
|
1139
|
+
if (!component) {
|
|
1140
|
+
return false;
|
|
1141
|
+
}
|
|
1142
|
+
await this.runtime.deleteComponent(component.id);
|
|
1143
|
+
logger3.debug(
|
|
1144
|
+
`[ComponentSecretStorage] Deleted secret: ${key} for user: ${context.userId}`
|
|
1145
|
+
);
|
|
1146
|
+
return true;
|
|
1147
|
+
}
|
|
1148
|
+
async list(context) {
|
|
1149
|
+
if (!context.userId) {
|
|
1150
|
+
return {};
|
|
1151
|
+
}
|
|
1152
|
+
if (context.requesterId && context.requesterId !== context.userId) {
|
|
1153
|
+
throw new PermissionDeniedError("*", "read", context);
|
|
1154
|
+
}
|
|
1155
|
+
const components = await this.runtime.getComponents(context.userId);
|
|
1156
|
+
const metadata = {};
|
|
1157
|
+
for (const component of components) {
|
|
1158
|
+
if (component.type !== COMPONENT_TYPE) {
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
1161
|
+
const data = component.data;
|
|
1162
|
+
if (!data?.key || !data?.config) {
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
if (data.config.expiresAt && data.config.expiresAt < Date.now()) {
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
metadata[data.key] = { ...data.config };
|
|
1169
|
+
}
|
|
1170
|
+
return metadata;
|
|
1171
|
+
}
|
|
1172
|
+
async getConfig(key, context) {
|
|
1173
|
+
if (!context.userId) {
|
|
1174
|
+
return null;
|
|
1175
|
+
}
|
|
1176
|
+
const component = await this.findSecretComponent(context.userId, key);
|
|
1177
|
+
if (!component) {
|
|
1178
|
+
return null;
|
|
1179
|
+
}
|
|
1180
|
+
const data = component.data;
|
|
1181
|
+
return data?.config ? { ...data.config } : null;
|
|
1182
|
+
}
|
|
1183
|
+
async updateConfig(key, context, config) {
|
|
1184
|
+
if (!context.userId) {
|
|
1185
|
+
return false;
|
|
1186
|
+
}
|
|
1187
|
+
if (context.requesterId && context.requesterId !== context.userId) {
|
|
1188
|
+
throw new PermissionDeniedError(key, "write", context);
|
|
1189
|
+
}
|
|
1190
|
+
const component = await this.findSecretComponent(context.userId, key);
|
|
1191
|
+
if (!component) {
|
|
1192
|
+
return false;
|
|
1193
|
+
}
|
|
1194
|
+
const data = component.data;
|
|
1195
|
+
if (!data) {
|
|
1196
|
+
return false;
|
|
1197
|
+
}
|
|
1198
|
+
data.config = {
|
|
1199
|
+
...data.config,
|
|
1200
|
+
...config
|
|
1201
|
+
};
|
|
1202
|
+
data.updatedAt = Date.now();
|
|
1203
|
+
await this.runtime.updateComponent({
|
|
1204
|
+
...component,
|
|
1205
|
+
data
|
|
1206
|
+
});
|
|
1207
|
+
return true;
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Find a secret component for a user by key
|
|
1211
|
+
*/
|
|
1212
|
+
async findSecretComponent(userId, key) {
|
|
1213
|
+
const components = await this.runtime.getComponents(userId);
|
|
1214
|
+
for (const component of components) {
|
|
1215
|
+
if (component.type !== COMPONENT_TYPE) {
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
const data = component.data;
|
|
1219
|
+
if (data?.key === key) {
|
|
1220
|
+
return component;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
return null;
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Get all secret keys for a user
|
|
1227
|
+
*/
|
|
1228
|
+
async listKeys(userId) {
|
|
1229
|
+
const components = await this.runtime.getComponents(userId);
|
|
1230
|
+
const keys = [];
|
|
1231
|
+
for (const component of components) {
|
|
1232
|
+
if (component.type !== COMPONENT_TYPE) {
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
const data = component.data;
|
|
1236
|
+
if (data?.key) {
|
|
1237
|
+
keys.push(data.key);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
return keys;
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Delete all secrets for a user
|
|
1244
|
+
*/
|
|
1245
|
+
async deleteAllForUser(userId) {
|
|
1246
|
+
const components = await this.runtime.getComponents(userId);
|
|
1247
|
+
let deleted = 0;
|
|
1248
|
+
for (const component of components) {
|
|
1249
|
+
if (component.type !== COMPONENT_TYPE) {
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
await this.runtime.deleteComponent(component.id);
|
|
1253
|
+
deleted++;
|
|
1254
|
+
}
|
|
1255
|
+
logger3.info(
|
|
1256
|
+
`[ComponentSecretStorage] Deleted ${deleted} secrets for user: ${userId}`
|
|
1257
|
+
);
|
|
1258
|
+
return deleted;
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Count secrets for a user
|
|
1262
|
+
*/
|
|
1263
|
+
async countForUser(userId) {
|
|
1264
|
+
const components = await this.runtime.getComponents(userId);
|
|
1265
|
+
let count = 0;
|
|
1266
|
+
for (const component of components) {
|
|
1267
|
+
if (component.type === COMPONENT_TYPE) {
|
|
1268
|
+
count++;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
return count;
|
|
1272
|
+
}
|
|
1273
|
+
};
|
|
1274
|
+
|
|
1275
|
+
// src/validation.ts
|
|
1276
|
+
import { logger as logger4 } from "@elizaos/core";
|
|
1277
|
+
var ValidationStrategies = {
|
|
1278
|
+
/**
|
|
1279
|
+
* No validation - always passes
|
|
1280
|
+
*/
|
|
1281
|
+
none: async (_key, _value) => ({
|
|
1282
|
+
isValid: true,
|
|
1283
|
+
validatedAt: Date.now()
|
|
1284
|
+
}),
|
|
1285
|
+
/**
|
|
1286
|
+
* OpenAI API key validation
|
|
1287
|
+
* Format: sk-... or sk-proj-...
|
|
1288
|
+
*/
|
|
1289
|
+
"api_key:openai": async (key, value) => {
|
|
1290
|
+
const validatedAt = Date.now();
|
|
1291
|
+
if (!value.startsWith("sk-")) {
|
|
1292
|
+
return {
|
|
1293
|
+
isValid: false,
|
|
1294
|
+
error: 'OpenAI API key must start with "sk-"',
|
|
1295
|
+
validatedAt
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
if (value.length < 20) {
|
|
1299
|
+
return {
|
|
1300
|
+
isValid: false,
|
|
1301
|
+
error: "OpenAI API key is too short",
|
|
1302
|
+
validatedAt
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
const shouldVerify = process.env.VALIDATE_API_KEYS === "true";
|
|
1306
|
+
if (shouldVerify) {
|
|
1307
|
+
const verified = await verifyOpenAIKey(value);
|
|
1308
|
+
if (!verified.isValid) {
|
|
1309
|
+
return { ...verified, validatedAt };
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
return { isValid: true, validatedAt };
|
|
1313
|
+
},
|
|
1314
|
+
/**
|
|
1315
|
+
* Anthropic API key validation
|
|
1316
|
+
* Format: sk-ant-...
|
|
1317
|
+
*/
|
|
1318
|
+
"api_key:anthropic": async (key, value) => {
|
|
1319
|
+
const validatedAt = Date.now();
|
|
1320
|
+
if (!value.startsWith("sk-ant-")) {
|
|
1321
|
+
return {
|
|
1322
|
+
isValid: false,
|
|
1323
|
+
error: 'Anthropic API key must start with "sk-ant-"',
|
|
1324
|
+
validatedAt
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
if (value.length < 30) {
|
|
1328
|
+
return {
|
|
1329
|
+
isValid: false,
|
|
1330
|
+
error: "Anthropic API key is too short",
|
|
1331
|
+
validatedAt
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
const shouldVerify = process.env.VALIDATE_API_KEYS === "true";
|
|
1335
|
+
if (shouldVerify) {
|
|
1336
|
+
const verified = await verifyAnthropicKey(value);
|
|
1337
|
+
if (!verified.isValid) {
|
|
1338
|
+
return { ...verified, validatedAt };
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
return { isValid: true, validatedAt };
|
|
1342
|
+
},
|
|
1343
|
+
/**
|
|
1344
|
+
* Groq API key validation
|
|
1345
|
+
* Format: gsk_...
|
|
1346
|
+
*/
|
|
1347
|
+
"api_key:groq": async (key, value) => {
|
|
1348
|
+
const validatedAt = Date.now();
|
|
1349
|
+
if (!value.startsWith("gsk_")) {
|
|
1350
|
+
return {
|
|
1351
|
+
isValid: false,
|
|
1352
|
+
error: 'Groq API key must start with "gsk_"',
|
|
1353
|
+
validatedAt
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
if (value.length < 20) {
|
|
1357
|
+
return {
|
|
1358
|
+
isValid: false,
|
|
1359
|
+
error: "Groq API key is too short",
|
|
1360
|
+
validatedAt
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
return { isValid: true, validatedAt };
|
|
1364
|
+
},
|
|
1365
|
+
/**
|
|
1366
|
+
* Google API key validation
|
|
1367
|
+
* Format: AIza...
|
|
1368
|
+
*/
|
|
1369
|
+
"api_key:google": async (key, value) => {
|
|
1370
|
+
const validatedAt = Date.now();
|
|
1371
|
+
if (!value.startsWith("AIza")) {
|
|
1372
|
+
return {
|
|
1373
|
+
isValid: false,
|
|
1374
|
+
error: 'Google API key must start with "AIza"',
|
|
1375
|
+
validatedAt
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
if (value.length < 30) {
|
|
1379
|
+
return {
|
|
1380
|
+
isValid: false,
|
|
1381
|
+
error: "Google API key is too short",
|
|
1382
|
+
validatedAt
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
return { isValid: true, validatedAt };
|
|
1386
|
+
},
|
|
1387
|
+
/**
|
|
1388
|
+
* Mistral API key validation
|
|
1389
|
+
*/
|
|
1390
|
+
"api_key:mistral": async (key, value) => {
|
|
1391
|
+
const validatedAt = Date.now();
|
|
1392
|
+
if (value.length < 20) {
|
|
1393
|
+
return {
|
|
1394
|
+
isValid: false,
|
|
1395
|
+
error: "Mistral API key is too short",
|
|
1396
|
+
validatedAt
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
return { isValid: true, validatedAt };
|
|
1400
|
+
},
|
|
1401
|
+
/**
|
|
1402
|
+
* Cohere API key validation
|
|
1403
|
+
*/
|
|
1404
|
+
"api_key:cohere": async (key, value) => {
|
|
1405
|
+
const validatedAt = Date.now();
|
|
1406
|
+
if (value.length < 20) {
|
|
1407
|
+
return {
|
|
1408
|
+
isValid: false,
|
|
1409
|
+
error: "Cohere API key is too short",
|
|
1410
|
+
validatedAt
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
return { isValid: true, validatedAt };
|
|
1414
|
+
},
|
|
1415
|
+
/**
|
|
1416
|
+
* URL format validation
|
|
1417
|
+
*/
|
|
1418
|
+
"url:valid": async (key, value) => {
|
|
1419
|
+
const validatedAt = Date.now();
|
|
1420
|
+
try {
|
|
1421
|
+
new URL(value);
|
|
1422
|
+
return { isValid: true, validatedAt };
|
|
1423
|
+
} catch {
|
|
1424
|
+
return {
|
|
1425
|
+
isValid: false,
|
|
1426
|
+
error: "Invalid URL format",
|
|
1427
|
+
validatedAt
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
},
|
|
1431
|
+
/**
|
|
1432
|
+
* URL reachability validation
|
|
1433
|
+
*/
|
|
1434
|
+
"url:reachable": async (key, value) => {
|
|
1435
|
+
const validatedAt = Date.now();
|
|
1436
|
+
try {
|
|
1437
|
+
new URL(value);
|
|
1438
|
+
} catch {
|
|
1439
|
+
return {
|
|
1440
|
+
isValid: false,
|
|
1441
|
+
error: "Invalid URL format",
|
|
1442
|
+
validatedAt
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
try {
|
|
1446
|
+
const response = await fetch(value, {
|
|
1447
|
+
method: "HEAD",
|
|
1448
|
+
signal: AbortSignal.timeout(5e3)
|
|
1449
|
+
});
|
|
1450
|
+
if (!response.ok) {
|
|
1451
|
+
return {
|
|
1452
|
+
isValid: false,
|
|
1453
|
+
error: `URL returned status ${response.status}`,
|
|
1454
|
+
validatedAt
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
return { isValid: true, validatedAt };
|
|
1458
|
+
} catch (error) {
|
|
1459
|
+
return {
|
|
1460
|
+
isValid: false,
|
|
1461
|
+
error: `URL is not reachable: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
1462
|
+
validatedAt
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
},
|
|
1466
|
+
/**
|
|
1467
|
+
* Custom validation placeholder
|
|
1468
|
+
*/
|
|
1469
|
+
custom: async (key, value) => {
|
|
1470
|
+
return {
|
|
1471
|
+
isValid: true,
|
|
1472
|
+
details: "Custom validation not implemented",
|
|
1473
|
+
validatedAt: Date.now()
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
};
|
|
1477
|
+
var customValidators = /* @__PURE__ */ new Map();
|
|
1478
|
+
function registerValidator(name, validator) {
|
|
1479
|
+
customValidators.set(name, validator);
|
|
1480
|
+
logger4.debug(`[Validation] Registered custom validator: ${name}`);
|
|
1481
|
+
}
|
|
1482
|
+
function unregisterValidator(name) {
|
|
1483
|
+
return customValidators.delete(name);
|
|
1484
|
+
}
|
|
1485
|
+
function getValidator(strategy) {
|
|
1486
|
+
if (strategy in ValidationStrategies) {
|
|
1487
|
+
return ValidationStrategies[strategy];
|
|
1488
|
+
}
|
|
1489
|
+
return customValidators.get(strategy);
|
|
1490
|
+
}
|
|
1491
|
+
async function validateSecret(key, value, strategy) {
|
|
1492
|
+
const strategyName = strategy ?? "none";
|
|
1493
|
+
const validator = getValidator(strategyName);
|
|
1494
|
+
if (!validator) {
|
|
1495
|
+
logger4.warn(`[Validation] Unknown validation strategy: ${strategyName}`);
|
|
1496
|
+
return {
|
|
1497
|
+
isValid: true,
|
|
1498
|
+
details: `Unknown validation strategy: ${strategyName}`,
|
|
1499
|
+
validatedAt: Date.now()
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
try {
|
|
1503
|
+
return await validator(key, value);
|
|
1504
|
+
} catch (error) {
|
|
1505
|
+
const errorMessage = error instanceof Error ? error.message : "Validation error";
|
|
1506
|
+
logger4.error(`[Validation] Error validating ${key}: ${errorMessage}`);
|
|
1507
|
+
return {
|
|
1508
|
+
isValid: false,
|
|
1509
|
+
error: errorMessage,
|
|
1510
|
+
validatedAt: Date.now()
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
function inferValidationStrategy(key) {
|
|
1515
|
+
const upperKey = key.toUpperCase();
|
|
1516
|
+
if (upperKey.includes("OPENAI") && upperKey.includes("KEY")) {
|
|
1517
|
+
return "api_key:openai";
|
|
1518
|
+
}
|
|
1519
|
+
if (upperKey.includes("ANTHROPIC") && upperKey.includes("KEY")) {
|
|
1520
|
+
return "api_key:anthropic";
|
|
1521
|
+
}
|
|
1522
|
+
if (upperKey.includes("GROQ") && upperKey.includes("KEY")) {
|
|
1523
|
+
return "api_key:groq";
|
|
1524
|
+
}
|
|
1525
|
+
if (upperKey.includes("GOOGLE") && upperKey.includes("KEY")) {
|
|
1526
|
+
return "api_key:google";
|
|
1527
|
+
}
|
|
1528
|
+
if (upperKey.includes("MISTRAL") && upperKey.includes("KEY")) {
|
|
1529
|
+
return "api_key:mistral";
|
|
1530
|
+
}
|
|
1531
|
+
if (upperKey.includes("COHERE") && upperKey.includes("KEY")) {
|
|
1532
|
+
return "api_key:cohere";
|
|
1533
|
+
}
|
|
1534
|
+
if (upperKey.includes("URL") || upperKey.includes("ENDPOINT")) {
|
|
1535
|
+
return "url:valid";
|
|
1536
|
+
}
|
|
1537
|
+
return "none";
|
|
1538
|
+
}
|
|
1539
|
+
async function verifyOpenAIKey(apiKey) {
|
|
1540
|
+
try {
|
|
1541
|
+
const response = await fetch("https://api.openai.com/v1/models", {
|
|
1542
|
+
method: "GET",
|
|
1543
|
+
headers: {
|
|
1544
|
+
Authorization: `Bearer ${apiKey}`
|
|
1545
|
+
},
|
|
1546
|
+
signal: AbortSignal.timeout(1e4)
|
|
1547
|
+
});
|
|
1548
|
+
if (response.status === 401) {
|
|
1549
|
+
return { isValid: false, error: "Invalid API key" };
|
|
1550
|
+
}
|
|
1551
|
+
if (response.status === 429) {
|
|
1552
|
+
return { isValid: true };
|
|
1553
|
+
}
|
|
1554
|
+
if (!response.ok) {
|
|
1555
|
+
return {
|
|
1556
|
+
isValid: false,
|
|
1557
|
+
error: `API returned status ${response.status}`
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
return { isValid: true };
|
|
1561
|
+
} catch (error) {
|
|
1562
|
+
return {
|
|
1563
|
+
isValid: false,
|
|
1564
|
+
error: `Failed to verify: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
async function verifyAnthropicKey(apiKey) {
|
|
1569
|
+
try {
|
|
1570
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
1571
|
+
method: "POST",
|
|
1572
|
+
headers: {
|
|
1573
|
+
"x-api-key": apiKey,
|
|
1574
|
+
"anthropic-version": "2023-06-01",
|
|
1575
|
+
"content-type": "application/json"
|
|
1576
|
+
},
|
|
1577
|
+
body: JSON.stringify({
|
|
1578
|
+
model: "claude-3-haiku-20240307",
|
|
1579
|
+
max_tokens: 1,
|
|
1580
|
+
messages: [{ role: "user", content: "Hi" }]
|
|
1581
|
+
}),
|
|
1582
|
+
signal: AbortSignal.timeout(1e4)
|
|
1583
|
+
});
|
|
1584
|
+
if (response.status === 401) {
|
|
1585
|
+
return { isValid: false, error: "Invalid API key" };
|
|
1586
|
+
}
|
|
1587
|
+
if (response.status === 429) {
|
|
1588
|
+
return { isValid: true };
|
|
1589
|
+
}
|
|
1590
|
+
if (response.status === 400) {
|
|
1591
|
+
const body = await response.json().catch(() => ({}));
|
|
1592
|
+
if (body.error?.type === "invalid_request_error") {
|
|
1593
|
+
return { isValid: true };
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
if (!response.ok && response.status !== 400) {
|
|
1597
|
+
return {
|
|
1598
|
+
isValid: false,
|
|
1599
|
+
error: `API returned status ${response.status}`
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
return { isValid: true };
|
|
1603
|
+
} catch (error) {
|
|
1604
|
+
return {
|
|
1605
|
+
isValid: false,
|
|
1606
|
+
error: `Failed to verify: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// src/services/secrets.ts
|
|
1612
|
+
var SECRETS_SERVICE_TYPE = "SECRETS";
|
|
1613
|
+
var DEFAULT_CONFIG = {
|
|
1614
|
+
enableEncryption: true,
|
|
1615
|
+
encryptionSalt: void 0,
|
|
1616
|
+
enableAccessLogging: true,
|
|
1617
|
+
maxAccessLogEntries: MAX_ACCESS_LOG_ENTRIES
|
|
1618
|
+
};
|
|
1619
|
+
var SecretsService = class _SecretsService extends Service {
|
|
1620
|
+
static serviceType = SECRETS_SERVICE_TYPE;
|
|
1621
|
+
capabilityDescription = "Manage secrets at global, world, and user levels with encryption and access control";
|
|
1622
|
+
secretsConfig;
|
|
1623
|
+
keyManager;
|
|
1624
|
+
storage;
|
|
1625
|
+
globalStorage;
|
|
1626
|
+
worldStorage;
|
|
1627
|
+
userStorage;
|
|
1628
|
+
accessLogs = [];
|
|
1629
|
+
changeCallbacks = /* @__PURE__ */ new Map();
|
|
1630
|
+
globalChangeCallbacks = [];
|
|
1631
|
+
constructor(runtime, config) {
|
|
1632
|
+
super(runtime);
|
|
1633
|
+
this.secretsConfig = { ...DEFAULT_CONFIG, ...config };
|
|
1634
|
+
this.keyManager = new KeyManager();
|
|
1635
|
+
if (runtime) {
|
|
1636
|
+
this.keyManager.initializeFromAgentId(
|
|
1637
|
+
runtime.agentId,
|
|
1638
|
+
this.secretsConfig.encryptionSalt ?? runtime.getSetting("ENCRYPTION_SALT")
|
|
1639
|
+
);
|
|
1640
|
+
this.globalStorage = new CharacterSettingsStorage(
|
|
1641
|
+
runtime,
|
|
1642
|
+
this.keyManager
|
|
1643
|
+
);
|
|
1644
|
+
this.worldStorage = new WorldMetadataStorage(runtime, this.keyManager);
|
|
1645
|
+
this.userStorage = new ComponentSecretStorage(runtime, this.keyManager);
|
|
1646
|
+
this.storage = new CompositeSecretStorage({
|
|
1647
|
+
globalStorage: this.globalStorage,
|
|
1648
|
+
worldStorage: this.worldStorage,
|
|
1649
|
+
userStorage: this.userStorage
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* Start the service
|
|
1655
|
+
*/
|
|
1656
|
+
static async start(runtime, config) {
|
|
1657
|
+
const service = new _SecretsService(runtime, config);
|
|
1658
|
+
await service.initialize();
|
|
1659
|
+
return service;
|
|
1660
|
+
}
|
|
1661
|
+
/**
|
|
1662
|
+
* Initialize the service
|
|
1663
|
+
*/
|
|
1664
|
+
async initialize() {
|
|
1665
|
+
logger5.info("[SecretsService] Initializing");
|
|
1666
|
+
await this.storage.initialize();
|
|
1667
|
+
const migrated = await this.globalStorage.migrateFromEnvVars();
|
|
1668
|
+
if (migrated > 0) {
|
|
1669
|
+
logger5.info(`[SecretsService] Migrated ${migrated} legacy env vars`);
|
|
1670
|
+
}
|
|
1671
|
+
const synced = await this.globalStorage.syncAllToEnv();
|
|
1672
|
+
logger5.debug(`[SecretsService] Synced ${synced} secrets to process.env`);
|
|
1673
|
+
logger5.info("[SecretsService] Initialized");
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* Stop the service
|
|
1677
|
+
*/
|
|
1678
|
+
async stop() {
|
|
1679
|
+
logger5.info("[SecretsService] Stopping");
|
|
1680
|
+
this.keyManager.clear();
|
|
1681
|
+
this.accessLogs = [];
|
|
1682
|
+
this.changeCallbacks.clear();
|
|
1683
|
+
this.globalChangeCallbacks = [];
|
|
1684
|
+
logger5.info("[SecretsService] Stopped");
|
|
1685
|
+
}
|
|
1686
|
+
// ============================================================================
|
|
1687
|
+
// Core Secret Operations
|
|
1688
|
+
// ============================================================================
|
|
1689
|
+
/**
|
|
1690
|
+
* Get a secret value
|
|
1691
|
+
*/
|
|
1692
|
+
async get(key, context) {
|
|
1693
|
+
this.logAccess(key, "read", context, true);
|
|
1694
|
+
const value = await this.storage.get(key, context);
|
|
1695
|
+
if (value === null) {
|
|
1696
|
+
this.logAccess(key, "read", context, false, "Secret not found");
|
|
1697
|
+
}
|
|
1698
|
+
return value;
|
|
1699
|
+
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Set a secret value
|
|
1702
|
+
*/
|
|
1703
|
+
async set(key, value, context, config) {
|
|
1704
|
+
this.logAccess(key, "write", context, true);
|
|
1705
|
+
if (config?.validationMethod && config.validationMethod !== "none") {
|
|
1706
|
+
const validation = await this.validate(
|
|
1707
|
+
key,
|
|
1708
|
+
value,
|
|
1709
|
+
config.validationMethod
|
|
1710
|
+
);
|
|
1711
|
+
if (!validation.isValid) {
|
|
1712
|
+
this.logAccess(
|
|
1713
|
+
key,
|
|
1714
|
+
"write",
|
|
1715
|
+
context,
|
|
1716
|
+
false,
|
|
1717
|
+
`Validation failed: ${validation.error}`
|
|
1718
|
+
);
|
|
1719
|
+
throw new SecretsError(
|
|
1720
|
+
`Validation failed for ${key}: ${validation.error}`,
|
|
1721
|
+
"VALIDATION_FAILED",
|
|
1722
|
+
{ key, error: validation.error }
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
const previousValue = await this.storage.get(key, context);
|
|
1727
|
+
const success = await this.storage.set(key, value, context, config);
|
|
1728
|
+
if (success) {
|
|
1729
|
+
if (context.level === "global") {
|
|
1730
|
+
await this.globalStorage.syncToEnv(key);
|
|
1731
|
+
}
|
|
1732
|
+
await this.emitChangeEvent({
|
|
1733
|
+
type: previousValue === null ? "created" : "updated",
|
|
1734
|
+
key,
|
|
1735
|
+
value,
|
|
1736
|
+
previousValue: previousValue ?? void 0,
|
|
1737
|
+
context,
|
|
1738
|
+
timestamp: Date.now()
|
|
1739
|
+
});
|
|
1740
|
+
} else {
|
|
1741
|
+
this.logAccess(key, "write", context, false, "Storage operation failed");
|
|
1742
|
+
}
|
|
1743
|
+
return success;
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Delete a secret
|
|
1747
|
+
*/
|
|
1748
|
+
async delete(key, context) {
|
|
1749
|
+
this.logAccess(key, "delete", context, true);
|
|
1750
|
+
const previousValue = await this.storage.get(key, context);
|
|
1751
|
+
const success = await this.storage.delete(key, context);
|
|
1752
|
+
if (success) {
|
|
1753
|
+
if (context.level === "global") {
|
|
1754
|
+
delete process.env[key];
|
|
1755
|
+
}
|
|
1756
|
+
await this.emitChangeEvent({
|
|
1757
|
+
type: "deleted",
|
|
1758
|
+
key,
|
|
1759
|
+
value: null,
|
|
1760
|
+
previousValue: previousValue ?? void 0,
|
|
1761
|
+
context,
|
|
1762
|
+
timestamp: Date.now()
|
|
1763
|
+
});
|
|
1764
|
+
} else {
|
|
1765
|
+
this.logAccess(key, "delete", context, false, "Secret not found");
|
|
1766
|
+
}
|
|
1767
|
+
return success;
|
|
1768
|
+
}
|
|
1769
|
+
/**
|
|
1770
|
+
* Check if a secret exists
|
|
1771
|
+
*/
|
|
1772
|
+
async exists(key, context) {
|
|
1773
|
+
return this.storage.exists(key, context);
|
|
1774
|
+
}
|
|
1775
|
+
/**
|
|
1776
|
+
* List secrets (metadata only, no values)
|
|
1777
|
+
*/
|
|
1778
|
+
async list(context) {
|
|
1779
|
+
return this.storage.list(context);
|
|
1780
|
+
}
|
|
1781
|
+
/**
|
|
1782
|
+
* Get secret configuration
|
|
1783
|
+
*/
|
|
1784
|
+
async getConfig(key, context) {
|
|
1785
|
+
return this.storage.getConfig(key, context);
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Update secret configuration
|
|
1789
|
+
*/
|
|
1790
|
+
async updateConfig(key, context, config) {
|
|
1791
|
+
return this.storage.updateConfig(key, context, config);
|
|
1792
|
+
}
|
|
1793
|
+
// ============================================================================
|
|
1794
|
+
// Convenience Methods
|
|
1795
|
+
// ============================================================================
|
|
1796
|
+
/**
|
|
1797
|
+
* Get a global secret (agent-level)
|
|
1798
|
+
*/
|
|
1799
|
+
async getGlobal(key) {
|
|
1800
|
+
return this.get(key, { level: "global", agentId: this.runtime.agentId });
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Set a global secret (agent-level)
|
|
1804
|
+
*/
|
|
1805
|
+
async setGlobal(key, value, config) {
|
|
1806
|
+
return this.set(
|
|
1807
|
+
key,
|
|
1808
|
+
value,
|
|
1809
|
+
{ level: "global", agentId: this.runtime.agentId },
|
|
1810
|
+
config
|
|
1811
|
+
);
|
|
1812
|
+
}
|
|
1813
|
+
/**
|
|
1814
|
+
* Get a world secret
|
|
1815
|
+
*/
|
|
1816
|
+
async getWorld(key, worldId) {
|
|
1817
|
+
return this.get(key, {
|
|
1818
|
+
level: "world",
|
|
1819
|
+
worldId,
|
|
1820
|
+
agentId: this.runtime.agentId
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
/**
|
|
1824
|
+
* Set a world secret
|
|
1825
|
+
*/
|
|
1826
|
+
async setWorld(key, value, worldId, config) {
|
|
1827
|
+
return this.set(
|
|
1828
|
+
key,
|
|
1829
|
+
value,
|
|
1830
|
+
{ level: "world", worldId, agentId: this.runtime.agentId },
|
|
1831
|
+
config
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1834
|
+
/**
|
|
1835
|
+
* Get a user secret
|
|
1836
|
+
*/
|
|
1837
|
+
async getUser(key, userId) {
|
|
1838
|
+
return this.get(key, {
|
|
1839
|
+
level: "user",
|
|
1840
|
+
userId,
|
|
1841
|
+
agentId: this.runtime.agentId,
|
|
1842
|
+
requesterId: userId
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
/**
|
|
1846
|
+
* Set a user secret
|
|
1847
|
+
*/
|
|
1848
|
+
async setUser(key, value, userId, config) {
|
|
1849
|
+
return this.set(
|
|
1850
|
+
key,
|
|
1851
|
+
value,
|
|
1852
|
+
{
|
|
1853
|
+
level: "user",
|
|
1854
|
+
userId,
|
|
1855
|
+
agentId: this.runtime.agentId,
|
|
1856
|
+
requesterId: userId
|
|
1857
|
+
},
|
|
1858
|
+
config
|
|
1859
|
+
);
|
|
1860
|
+
}
|
|
1861
|
+
// ============================================================================
|
|
1862
|
+
// Validation
|
|
1863
|
+
// ============================================================================
|
|
1864
|
+
/**
|
|
1865
|
+
* Validate a secret value
|
|
1866
|
+
*/
|
|
1867
|
+
async validate(key, value, strategy) {
|
|
1868
|
+
return validateSecret(key, value, strategy);
|
|
1869
|
+
}
|
|
1870
|
+
/**
|
|
1871
|
+
* Get available validation strategies
|
|
1872
|
+
*/
|
|
1873
|
+
getValidationStrategies() {
|
|
1874
|
+
return Object.keys(ValidationStrategies);
|
|
1875
|
+
}
|
|
1876
|
+
// ============================================================================
|
|
1877
|
+
// Plugin Requirements
|
|
1878
|
+
// ============================================================================
|
|
1879
|
+
/**
|
|
1880
|
+
* Check which secrets are missing for a plugin
|
|
1881
|
+
*/
|
|
1882
|
+
async checkPluginRequirements(pluginId, requirements) {
|
|
1883
|
+
const missingRequired = [];
|
|
1884
|
+
const missingOptional = [];
|
|
1885
|
+
const invalid = [];
|
|
1886
|
+
for (const [key, requirement] of Object.entries(requirements)) {
|
|
1887
|
+
const value = await this.getGlobal(key);
|
|
1888
|
+
if (value === null) {
|
|
1889
|
+
if (requirement.required) {
|
|
1890
|
+
missingRequired.push(key);
|
|
1891
|
+
} else {
|
|
1892
|
+
missingOptional.push(key);
|
|
1893
|
+
}
|
|
1894
|
+
continue;
|
|
1895
|
+
}
|
|
1896
|
+
if (requirement.validationMethod && requirement.validationMethod !== "none") {
|
|
1897
|
+
const validation = await this.validate(
|
|
1898
|
+
key,
|
|
1899
|
+
value,
|
|
1900
|
+
requirement.validationMethod
|
|
1901
|
+
);
|
|
1902
|
+
if (!validation.isValid) {
|
|
1903
|
+
invalid.push(key);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
return {
|
|
1908
|
+
ready: missingRequired.length === 0 && invalid.length === 0,
|
|
1909
|
+
missingRequired,
|
|
1910
|
+
missingOptional,
|
|
1911
|
+
invalid
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Get missing secrets for a set of keys
|
|
1916
|
+
*/
|
|
1917
|
+
async getMissingSecrets(keys, level = "global") {
|
|
1918
|
+
const missing = [];
|
|
1919
|
+
for (const key of keys) {
|
|
1920
|
+
let exists;
|
|
1921
|
+
switch (level) {
|
|
1922
|
+
case "global":
|
|
1923
|
+
exists = await this.exists(key, {
|
|
1924
|
+
level: "global",
|
|
1925
|
+
agentId: this.runtime.agentId
|
|
1926
|
+
});
|
|
1927
|
+
break;
|
|
1928
|
+
case "world":
|
|
1929
|
+
case "user":
|
|
1930
|
+
exists = false;
|
|
1931
|
+
break;
|
|
1932
|
+
default:
|
|
1933
|
+
exists = false;
|
|
1934
|
+
}
|
|
1935
|
+
if (!exists) {
|
|
1936
|
+
missing.push(key);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
return missing;
|
|
1940
|
+
}
|
|
1941
|
+
// ============================================================================
|
|
1942
|
+
// Change Notifications
|
|
1943
|
+
// ============================================================================
|
|
1944
|
+
/**
|
|
1945
|
+
* Register a callback for changes to a specific secret
|
|
1946
|
+
*/
|
|
1947
|
+
onSecretChanged(key, callback) {
|
|
1948
|
+
const callbacks = this.changeCallbacks.get(key) ?? [];
|
|
1949
|
+
callbacks.push(callback);
|
|
1950
|
+
this.changeCallbacks.set(key, callbacks);
|
|
1951
|
+
return () => {
|
|
1952
|
+
const cbs = this.changeCallbacks.get(key);
|
|
1953
|
+
if (cbs) {
|
|
1954
|
+
const index = cbs.indexOf(callback);
|
|
1955
|
+
if (index !== -1) {
|
|
1956
|
+
cbs.splice(index, 1);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1961
|
+
/**
|
|
1962
|
+
* Register a callback for all secret changes
|
|
1963
|
+
*/
|
|
1964
|
+
onAnySecretChanged(callback) {
|
|
1965
|
+
this.globalChangeCallbacks.push(callback);
|
|
1966
|
+
return () => {
|
|
1967
|
+
const index = this.globalChangeCallbacks.indexOf(callback);
|
|
1968
|
+
if (index !== -1) {
|
|
1969
|
+
this.globalChangeCallbacks.splice(index, 1);
|
|
1970
|
+
}
|
|
1971
|
+
};
|
|
1972
|
+
}
|
|
1973
|
+
/**
|
|
1974
|
+
* Emit a change event to registered callbacks
|
|
1975
|
+
*/
|
|
1976
|
+
async emitChangeEvent(event) {
|
|
1977
|
+
const keyCallbacks = this.changeCallbacks.get(event.key) ?? [];
|
|
1978
|
+
for (const callback of keyCallbacks) {
|
|
1979
|
+
await callback(event.key, event.value, event.context);
|
|
1980
|
+
}
|
|
1981
|
+
for (const callback of this.globalChangeCallbacks) {
|
|
1982
|
+
await callback(event.key, event.value, event.context);
|
|
1983
|
+
}
|
|
1984
|
+
logger5.debug(
|
|
1985
|
+
`[SecretsService] Emitted ${event.type} event for ${event.key}`
|
|
1986
|
+
);
|
|
1987
|
+
}
|
|
1988
|
+
// ============================================================================
|
|
1989
|
+
// Access Logging
|
|
1990
|
+
// ============================================================================
|
|
1991
|
+
/**
|
|
1992
|
+
* Log a secret access attempt
|
|
1993
|
+
*/
|
|
1994
|
+
logAccess(key, action, context, success, error) {
|
|
1995
|
+
if (!this.secretsConfig.enableAccessLogging) {
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
const log = {
|
|
1999
|
+
secretKey: key,
|
|
2000
|
+
accessedBy: context.requesterId ?? context.userId ?? context.agentId,
|
|
2001
|
+
action,
|
|
2002
|
+
timestamp: Date.now(),
|
|
2003
|
+
context,
|
|
2004
|
+
success,
|
|
2005
|
+
error
|
|
2006
|
+
};
|
|
2007
|
+
this.accessLogs.push(log);
|
|
2008
|
+
if (this.accessLogs.length > this.secretsConfig.maxAccessLogEntries) {
|
|
2009
|
+
this.accessLogs = this.accessLogs.slice(
|
|
2010
|
+
-this.secretsConfig.maxAccessLogEntries
|
|
2011
|
+
);
|
|
2012
|
+
}
|
|
2013
|
+
if (!success && error) {
|
|
2014
|
+
logger5.debug(
|
|
2015
|
+
`[SecretsService] Access denied: ${action} ${key} - ${error}`
|
|
2016
|
+
);
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
/**
|
|
2020
|
+
* Get access logs
|
|
2021
|
+
*/
|
|
2022
|
+
getAccessLogs(filter) {
|
|
2023
|
+
let logs = [...this.accessLogs];
|
|
2024
|
+
if (filter?.key) {
|
|
2025
|
+
logs = logs.filter((l) => l.secretKey === filter.key);
|
|
2026
|
+
}
|
|
2027
|
+
if (filter?.action) {
|
|
2028
|
+
logs = logs.filter((l) => l.action === filter.action);
|
|
2029
|
+
}
|
|
2030
|
+
if (filter?.since) {
|
|
2031
|
+
logs = logs.filter((l) => l.timestamp >= filter.since);
|
|
2032
|
+
}
|
|
2033
|
+
if (filter?.context) {
|
|
2034
|
+
logs = logs.filter((l) => {
|
|
2035
|
+
if (filter.context.level && l.context.level !== filter.context.level)
|
|
2036
|
+
return false;
|
|
2037
|
+
if (filter.context.worldId && l.context.worldId !== filter.context.worldId)
|
|
2038
|
+
return false;
|
|
2039
|
+
if (filter.context.userId && l.context.userId !== filter.context.userId)
|
|
2040
|
+
return false;
|
|
2041
|
+
return true;
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
return logs;
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* Clear access logs
|
|
2048
|
+
*/
|
|
2049
|
+
clearAccessLogs() {
|
|
2050
|
+
this.accessLogs = [];
|
|
2051
|
+
}
|
|
2052
|
+
// ============================================================================
|
|
2053
|
+
// Storage Access
|
|
2054
|
+
// ============================================================================
|
|
2055
|
+
/**
|
|
2056
|
+
* Get the global storage backend
|
|
2057
|
+
*/
|
|
2058
|
+
getGlobalStorage() {
|
|
2059
|
+
return this.globalStorage;
|
|
2060
|
+
}
|
|
2061
|
+
/**
|
|
2062
|
+
* Get the world storage backend
|
|
2063
|
+
*/
|
|
2064
|
+
getWorldStorage() {
|
|
2065
|
+
return this.worldStorage;
|
|
2066
|
+
}
|
|
2067
|
+
/**
|
|
2068
|
+
* Get the user storage backend
|
|
2069
|
+
*/
|
|
2070
|
+
getUserStorage() {
|
|
2071
|
+
return this.userStorage;
|
|
2072
|
+
}
|
|
2073
|
+
/**
|
|
2074
|
+
* Get the key manager (for advanced use cases)
|
|
2075
|
+
*/
|
|
2076
|
+
getKeyManager() {
|
|
2077
|
+
return this.keyManager;
|
|
2078
|
+
}
|
|
2079
|
+
};
|
|
2080
|
+
|
|
2081
|
+
// src/services/plugin-activator.ts
|
|
2082
|
+
import {
|
|
2083
|
+
Service as Service2,
|
|
2084
|
+
logger as logger6
|
|
2085
|
+
} from "@elizaos/core";
|
|
2086
|
+
var PLUGIN_ACTIVATOR_SERVICE_TYPE = "PLUGIN_ACTIVATOR";
|
|
2087
|
+
var DEFAULT_CONFIG2 = {
|
|
2088
|
+
enableAutoActivation: true,
|
|
2089
|
+
pollingIntervalMs: 5e3,
|
|
2090
|
+
maxWaitMs: 0
|
|
2091
|
+
// 0 = wait forever
|
|
2092
|
+
};
|
|
2093
|
+
var PluginActivatorService = class _PluginActivatorService extends Service2 {
|
|
2094
|
+
static serviceType = PLUGIN_ACTIVATOR_SERVICE_TYPE;
|
|
2095
|
+
capabilityDescription = "Activate plugins dynamically when their required secrets become available";
|
|
2096
|
+
activatorConfig;
|
|
2097
|
+
secretsService = null;
|
|
2098
|
+
pendingPlugins = /* @__PURE__ */ new Map();
|
|
2099
|
+
activatedPlugins = /* @__PURE__ */ new Set();
|
|
2100
|
+
pluginSecretMapping = /* @__PURE__ */ new Map();
|
|
2101
|
+
pollingInterval = null;
|
|
2102
|
+
unsubscribeSecretChanges = null;
|
|
2103
|
+
constructor(runtime, config) {
|
|
2104
|
+
super(runtime);
|
|
2105
|
+
this.activatorConfig = { ...DEFAULT_CONFIG2, ...config };
|
|
2106
|
+
}
|
|
2107
|
+
/**
|
|
2108
|
+
* Start the service
|
|
2109
|
+
*/
|
|
2110
|
+
static async start(runtime, config) {
|
|
2111
|
+
const service = new _PluginActivatorService(runtime, config);
|
|
2112
|
+
await service.initialize();
|
|
2113
|
+
return service;
|
|
2114
|
+
}
|
|
2115
|
+
/**
|
|
2116
|
+
* Initialize the service
|
|
2117
|
+
*/
|
|
2118
|
+
async initialize() {
|
|
2119
|
+
logger6.info("[PluginActivator] Initializing");
|
|
2120
|
+
this.secretsService = this.runtime.getService(SECRETS_SERVICE_TYPE);
|
|
2121
|
+
if (!this.secretsService) {
|
|
2122
|
+
logger6.warn(
|
|
2123
|
+
"[PluginActivator] SecretsService not available, activation will be limited"
|
|
2124
|
+
);
|
|
2125
|
+
} else {
|
|
2126
|
+
this.unsubscribeSecretChanges = this.secretsService.onAnySecretChanged(
|
|
2127
|
+
async (key, value, context) => {
|
|
2128
|
+
await this.onSecretChanged(key, value, context);
|
|
2129
|
+
}
|
|
2130
|
+
);
|
|
2131
|
+
}
|
|
2132
|
+
if (this.activatorConfig.enableAutoActivation && this.activatorConfig.pollingIntervalMs > 0) {
|
|
2133
|
+
this.startPolling();
|
|
2134
|
+
}
|
|
2135
|
+
logger6.info("[PluginActivator] Initialized");
|
|
2136
|
+
}
|
|
2137
|
+
/**
|
|
2138
|
+
* Stop the service
|
|
2139
|
+
*/
|
|
2140
|
+
async stop() {
|
|
2141
|
+
logger6.info("[PluginActivator] Stopping");
|
|
2142
|
+
if (this.pollingInterval) {
|
|
2143
|
+
clearInterval(this.pollingInterval);
|
|
2144
|
+
this.pollingInterval = null;
|
|
2145
|
+
}
|
|
2146
|
+
if (this.unsubscribeSecretChanges) {
|
|
2147
|
+
this.unsubscribeSecretChanges();
|
|
2148
|
+
this.unsubscribeSecretChanges = null;
|
|
2149
|
+
}
|
|
2150
|
+
this.pendingPlugins.clear();
|
|
2151
|
+
this.activatedPlugins.clear();
|
|
2152
|
+
this.pluginSecretMapping.clear();
|
|
2153
|
+
logger6.info("[PluginActivator] Stopped");
|
|
2154
|
+
}
|
|
2155
|
+
// ============================================================================
|
|
2156
|
+
// Plugin Registration
|
|
2157
|
+
// ============================================================================
|
|
2158
|
+
/**
|
|
2159
|
+
* Register a plugin for activation when secrets are ready
|
|
2160
|
+
*/
|
|
2161
|
+
async registerPlugin(plugin, activationCallback) {
|
|
2162
|
+
const pluginId = plugin.name;
|
|
2163
|
+
if (this.activatedPlugins.has(pluginId)) {
|
|
2164
|
+
logger6.debug(`[PluginActivator] Plugin ${pluginId} already activated`);
|
|
2165
|
+
return true;
|
|
2166
|
+
}
|
|
2167
|
+
if (!plugin.requiredSecrets || Object.keys(plugin.requiredSecrets).length === 0) {
|
|
2168
|
+
logger6.info(
|
|
2169
|
+
`[PluginActivator] Plugin ${pluginId} has no secret requirements, activating`
|
|
2170
|
+
);
|
|
2171
|
+
return this.activatePlugin(pluginId, plugin, activationCallback);
|
|
2172
|
+
}
|
|
2173
|
+
const status = await this.checkPluginRequirements(plugin);
|
|
2174
|
+
if (status.ready) {
|
|
2175
|
+
logger6.info(
|
|
2176
|
+
`[PluginActivator] Plugin ${pluginId} has all required secrets, activating`
|
|
2177
|
+
);
|
|
2178
|
+
return this.activatePlugin(pluginId, plugin, activationCallback);
|
|
2179
|
+
}
|
|
2180
|
+
logger6.info(
|
|
2181
|
+
`[PluginActivator] Plugin ${pluginId} queued, waiting for: ${status.missingRequired.join(", ")}`
|
|
2182
|
+
);
|
|
2183
|
+
const requiredSecrets = Object.entries(plugin.requiredSecrets).filter(([_, req]) => req.required).map(([key]) => key);
|
|
2184
|
+
this.pendingPlugins.set(pluginId, {
|
|
2185
|
+
pluginId,
|
|
2186
|
+
requiredSecrets,
|
|
2187
|
+
callback: async () => {
|
|
2188
|
+
await this.activatePlugin(pluginId, plugin, activationCallback);
|
|
2189
|
+
},
|
|
2190
|
+
registeredAt: Date.now()
|
|
2191
|
+
});
|
|
2192
|
+
for (const secretKey of requiredSecrets) {
|
|
2193
|
+
const plugins = this.pluginSecretMapping.get(secretKey) ?? /* @__PURE__ */ new Set();
|
|
2194
|
+
plugins.add(pluginId);
|
|
2195
|
+
this.pluginSecretMapping.set(secretKey, plugins);
|
|
2196
|
+
}
|
|
2197
|
+
return false;
|
|
2198
|
+
}
|
|
2199
|
+
/**
|
|
2200
|
+
* Unregister a pending plugin
|
|
2201
|
+
*/
|
|
2202
|
+
unregisterPlugin(pluginId) {
|
|
2203
|
+
const pending = this.pendingPlugins.get(pluginId);
|
|
2204
|
+
if (!pending) {
|
|
2205
|
+
return false;
|
|
2206
|
+
}
|
|
2207
|
+
for (const secretKey of pending.requiredSecrets) {
|
|
2208
|
+
const plugins = this.pluginSecretMapping.get(secretKey);
|
|
2209
|
+
if (plugins) {
|
|
2210
|
+
plugins.delete(pluginId);
|
|
2211
|
+
if (plugins.size === 0) {
|
|
2212
|
+
this.pluginSecretMapping.delete(secretKey);
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
this.pendingPlugins.delete(pluginId);
|
|
2217
|
+
logger6.info(`[PluginActivator] Unregistered plugin ${pluginId}`);
|
|
2218
|
+
return true;
|
|
2219
|
+
}
|
|
2220
|
+
// ============================================================================
|
|
2221
|
+
// Plugin Activation
|
|
2222
|
+
// ============================================================================
|
|
2223
|
+
/**
|
|
2224
|
+
* Activate a plugin
|
|
2225
|
+
*/
|
|
2226
|
+
async activatePlugin(pluginId, plugin, callback) {
|
|
2227
|
+
try {
|
|
2228
|
+
if (callback) {
|
|
2229
|
+
await callback();
|
|
2230
|
+
}
|
|
2231
|
+
if (plugin.onSecretsReady) {
|
|
2232
|
+
await plugin.onSecretsReady(this.runtime);
|
|
2233
|
+
}
|
|
2234
|
+
this.activatedPlugins.add(pluginId);
|
|
2235
|
+
this.pendingPlugins.delete(pluginId);
|
|
2236
|
+
logger6.info(`[PluginActivator] Activated plugin ${pluginId}`);
|
|
2237
|
+
return true;
|
|
2238
|
+
} catch (error) {
|
|
2239
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2240
|
+
logger6.error(
|
|
2241
|
+
`[PluginActivator] Failed to activate plugin ${pluginId}: ${errorMessage}`
|
|
2242
|
+
);
|
|
2243
|
+
return false;
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
/**
|
|
2247
|
+
* Check requirements for a plugin
|
|
2248
|
+
*/
|
|
2249
|
+
async checkPluginRequirements(plugin) {
|
|
2250
|
+
if (!plugin.requiredSecrets) {
|
|
2251
|
+
return {
|
|
2252
|
+
pluginId: plugin.name,
|
|
2253
|
+
ready: true,
|
|
2254
|
+
missingRequired: [],
|
|
2255
|
+
missingOptional: [],
|
|
2256
|
+
invalid: [],
|
|
2257
|
+
message: "No secrets required"
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
if (!this.secretsService) {
|
|
2261
|
+
const required = Object.entries(plugin.requiredSecrets).filter(([_, req]) => req.required).map(([key]) => key);
|
|
2262
|
+
return {
|
|
2263
|
+
pluginId: plugin.name,
|
|
2264
|
+
ready: required.length === 0,
|
|
2265
|
+
missingRequired: required,
|
|
2266
|
+
missingOptional: [],
|
|
2267
|
+
invalid: [],
|
|
2268
|
+
message: "SecretsService not available"
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
const result = await this.secretsService.checkPluginRequirements(
|
|
2272
|
+
plugin.name,
|
|
2273
|
+
plugin.requiredSecrets
|
|
2274
|
+
);
|
|
2275
|
+
return {
|
|
2276
|
+
pluginId: plugin.name,
|
|
2277
|
+
...result,
|
|
2278
|
+
message: result.ready ? "All secrets available" : `Missing: ${result.missingRequired.join(", ")}`
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
/**
|
|
2282
|
+
* Get status of all registered plugins
|
|
2283
|
+
*/
|
|
2284
|
+
getPluginStatuses() {
|
|
2285
|
+
const statuses = /* @__PURE__ */ new Map();
|
|
2286
|
+
for (const [pluginId, pending] of this.pendingPlugins) {
|
|
2287
|
+
statuses.set(pluginId, {
|
|
2288
|
+
pending: true,
|
|
2289
|
+
activated: false,
|
|
2290
|
+
missingSecrets: pending.requiredSecrets
|
|
2291
|
+
});
|
|
2292
|
+
}
|
|
2293
|
+
for (const pluginId of this.activatedPlugins) {
|
|
2294
|
+
statuses.set(pluginId, {
|
|
2295
|
+
pending: false,
|
|
2296
|
+
activated: true,
|
|
2297
|
+
missingSecrets: []
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
return statuses;
|
|
2301
|
+
}
|
|
2302
|
+
// ============================================================================
|
|
2303
|
+
// Secret Change Handling
|
|
2304
|
+
// ============================================================================
|
|
2305
|
+
/**
|
|
2306
|
+
* Handle secret change event
|
|
2307
|
+
*/
|
|
2308
|
+
async onSecretChanged(key, value, context) {
|
|
2309
|
+
if (context.level !== "global") {
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
2312
|
+
const affectedPlugins = this.pluginSecretMapping.get(key);
|
|
2313
|
+
if (!affectedPlugins || affectedPlugins.size === 0) {
|
|
2314
|
+
return;
|
|
2315
|
+
}
|
|
2316
|
+
logger6.debug(
|
|
2317
|
+
`[PluginActivator] Secret ${key} changed, checking ${affectedPlugins.size} plugins`
|
|
2318
|
+
);
|
|
2319
|
+
for (const pluginId of affectedPlugins) {
|
|
2320
|
+
const pending = this.pendingPlugins.get(pluginId);
|
|
2321
|
+
if (!pending) {
|
|
2322
|
+
continue;
|
|
2323
|
+
}
|
|
2324
|
+
const missing = await this.getMissingSecrets(pending.requiredSecrets);
|
|
2325
|
+
if (missing.length === 0) {
|
|
2326
|
+
logger6.info(
|
|
2327
|
+
`[PluginActivator] All secrets available for ${pluginId}, activating`
|
|
2328
|
+
);
|
|
2329
|
+
await pending.callback();
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
/**
|
|
2334
|
+
* Get missing secrets from a list
|
|
2335
|
+
*/
|
|
2336
|
+
async getMissingSecrets(keys) {
|
|
2337
|
+
if (!this.secretsService) {
|
|
2338
|
+
return keys;
|
|
2339
|
+
}
|
|
2340
|
+
return this.secretsService.getMissingSecrets(keys, "global");
|
|
2341
|
+
}
|
|
2342
|
+
// ============================================================================
|
|
2343
|
+
// Polling
|
|
2344
|
+
// ============================================================================
|
|
2345
|
+
/**
|
|
2346
|
+
* Start polling for pending plugins
|
|
2347
|
+
*/
|
|
2348
|
+
startPolling() {
|
|
2349
|
+
if (this.pollingInterval) {
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
this.pollingInterval = setInterval(async () => {
|
|
2353
|
+
await this.checkPendingPlugins();
|
|
2354
|
+
}, this.activatorConfig.pollingIntervalMs);
|
|
2355
|
+
logger6.debug(
|
|
2356
|
+
`[PluginActivator] Started polling every ${this.activatorConfig.pollingIntervalMs}ms`
|
|
2357
|
+
);
|
|
2358
|
+
}
|
|
2359
|
+
/**
|
|
2360
|
+
* Stop polling
|
|
2361
|
+
*/
|
|
2362
|
+
stopPolling() {
|
|
2363
|
+
if (this.pollingInterval) {
|
|
2364
|
+
clearInterval(this.pollingInterval);
|
|
2365
|
+
this.pollingInterval = null;
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
/**
|
|
2369
|
+
* Check all pending plugins
|
|
2370
|
+
*/
|
|
2371
|
+
async checkPendingPlugins() {
|
|
2372
|
+
if (this.pendingPlugins.size === 0) {
|
|
2373
|
+
return;
|
|
2374
|
+
}
|
|
2375
|
+
const now = Date.now();
|
|
2376
|
+
for (const [pluginId, pending] of this.pendingPlugins) {
|
|
2377
|
+
if (this.activatorConfig.maxWaitMs > 0) {
|
|
2378
|
+
const elapsed = now - pending.registeredAt;
|
|
2379
|
+
if (elapsed > this.activatorConfig.maxWaitMs) {
|
|
2380
|
+
logger6.warn(
|
|
2381
|
+
`[PluginActivator] Plugin ${pluginId} timed out waiting for secrets`
|
|
2382
|
+
);
|
|
2383
|
+
this.unregisterPlugin(pluginId);
|
|
2384
|
+
continue;
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
const missing = await this.getMissingSecrets(pending.requiredSecrets);
|
|
2388
|
+
if (missing.length === 0) {
|
|
2389
|
+
logger6.info(`[PluginActivator] Secrets now available for ${pluginId}`);
|
|
2390
|
+
await pending.callback();
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
// ============================================================================
|
|
2395
|
+
// Utility Methods
|
|
2396
|
+
// ============================================================================
|
|
2397
|
+
/**
|
|
2398
|
+
* Get list of pending plugins
|
|
2399
|
+
*/
|
|
2400
|
+
getPendingPlugins() {
|
|
2401
|
+
return Array.from(this.pendingPlugins.keys());
|
|
2402
|
+
}
|
|
2403
|
+
/**
|
|
2404
|
+
* Get list of activated plugins
|
|
2405
|
+
*/
|
|
2406
|
+
getActivatedPlugins() {
|
|
2407
|
+
return Array.from(this.activatedPlugins);
|
|
2408
|
+
}
|
|
2409
|
+
/**
|
|
2410
|
+
* Check if a plugin is pending
|
|
2411
|
+
*/
|
|
2412
|
+
isPending(pluginId) {
|
|
2413
|
+
return this.pendingPlugins.has(pluginId);
|
|
2414
|
+
}
|
|
2415
|
+
/**
|
|
2416
|
+
* Check if a plugin is activated
|
|
2417
|
+
*/
|
|
2418
|
+
isActivated(pluginId) {
|
|
2419
|
+
return this.activatedPlugins.has(pluginId);
|
|
2420
|
+
}
|
|
2421
|
+
/**
|
|
2422
|
+
* Get secrets required by pending plugins
|
|
2423
|
+
*/
|
|
2424
|
+
getRequiredSecrets() {
|
|
2425
|
+
const secrets = /* @__PURE__ */ new Set();
|
|
2426
|
+
for (const pending of this.pendingPlugins.values()) {
|
|
2427
|
+
for (const key of pending.requiredSecrets) {
|
|
2428
|
+
secrets.add(key);
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
return secrets;
|
|
2432
|
+
}
|
|
2433
|
+
/**
|
|
2434
|
+
* Get plugins waiting for a specific secret
|
|
2435
|
+
*/
|
|
2436
|
+
getPluginsWaitingFor(secretKey) {
|
|
2437
|
+
const plugins = this.pluginSecretMapping.get(secretKey);
|
|
2438
|
+
return plugins ? Array.from(plugins) : [];
|
|
2439
|
+
}
|
|
2440
|
+
};
|
|
2441
|
+
|
|
2442
|
+
// src/actions/set-secret.ts
|
|
2443
|
+
import {
|
|
2444
|
+
ModelType,
|
|
2445
|
+
logger as logger7,
|
|
2446
|
+
composePromptFromState
|
|
2447
|
+
} from "@elizaos/core";
|
|
2448
|
+
var extractSecretsTemplate = `You are extracting secret/configuration values from the user's message.
|
|
2449
|
+
|
|
2450
|
+
The user wants to set one or more secrets. Extract:
|
|
2451
|
+
1. The secret key (should be UPPERCASE_WITH_UNDERSCORES format)
|
|
2452
|
+
2. The secret value
|
|
2453
|
+
3. Optional description
|
|
2454
|
+
4. Secret type (api_key, secret, credential, url, or config)
|
|
2455
|
+
|
|
2456
|
+
Common patterns:
|
|
2457
|
+
- "Set my OpenAI key to sk-..." -> key: OPENAI_API_KEY, value: sk-...
|
|
2458
|
+
- "My Anthropic API key is sk-ant-..." -> key: ANTHROPIC_API_KEY, value: sk-ant-...
|
|
2459
|
+
- "Use this Discord token: ..." -> key: DISCORD_BOT_TOKEN, value: ...
|
|
2460
|
+
- "Set DATABASE_URL to postgres://..." -> key: DATABASE_URL, value: postgres://...
|
|
2461
|
+
|
|
2462
|
+
{{recentMessages}}
|
|
2463
|
+
|
|
2464
|
+
Extract the secrets from the user's message. If the key name isn't explicitly specified, infer an appropriate UPPERCASE_WITH_UNDERSCORES name based on the context.`;
|
|
2465
|
+
var setSecretAction = {
|
|
2466
|
+
name: "SET_SECRET",
|
|
2467
|
+
similes: [
|
|
2468
|
+
"STORE_SECRET",
|
|
2469
|
+
"SAVE_SECRET",
|
|
2470
|
+
"SET_API_KEY",
|
|
2471
|
+
"CONFIGURE_SECRET",
|
|
2472
|
+
"SET_ENV_VAR",
|
|
2473
|
+
"STORE_API_KEY",
|
|
2474
|
+
"SET_TOKEN",
|
|
2475
|
+
"SAVE_KEY"
|
|
2476
|
+
],
|
|
2477
|
+
description: "Set a secret value (API key, token, password, etc.) for the agent to use",
|
|
2478
|
+
validate: async (runtime, message, state) => {
|
|
2479
|
+
const text = message.content.text?.toLowerCase() ?? "";
|
|
2480
|
+
const setPatterns = [
|
|
2481
|
+
/\bset\b.*\b(key|token|secret|password|credential|api)/i,
|
|
2482
|
+
/\bmy\b.*\b(key|token|secret|api)\b.*\bis\b/i,
|
|
2483
|
+
/\buse\b.*\b(key|token|this)\b/i,
|
|
2484
|
+
/\bstore\b.*\b(key|token|secret)/i,
|
|
2485
|
+
/\bconfigure\b.*\b(key|token|secret)/i,
|
|
2486
|
+
/\bsave\b.*\b(key|token|secret)/i,
|
|
2487
|
+
/sk-[a-zA-Z0-9]+/i,
|
|
2488
|
+
// OpenAI key pattern
|
|
2489
|
+
/sk-ant-[a-zA-Z0-9]+/i,
|
|
2490
|
+
// Anthropic key pattern
|
|
2491
|
+
/gsk_[a-zA-Z0-9]+/i
|
|
2492
|
+
// Groq key pattern
|
|
2493
|
+
];
|
|
2494
|
+
const hasIntent = setPatterns.some((pattern) => pattern.test(text));
|
|
2495
|
+
if (!hasIntent) {
|
|
2496
|
+
return false;
|
|
2497
|
+
}
|
|
2498
|
+
const secretsService = runtime.getService(SECRETS_SERVICE_TYPE);
|
|
2499
|
+
return secretsService !== null;
|
|
2500
|
+
},
|
|
2501
|
+
handler: async (runtime, message, state, _options, callback) => {
|
|
2502
|
+
logger7.info("[SetSecret] Processing secret set request");
|
|
2503
|
+
const secretsService = runtime.getService(SECRETS_SERVICE_TYPE);
|
|
2504
|
+
if (!secretsService) {
|
|
2505
|
+
if (callback) {
|
|
2506
|
+
await callback({
|
|
2507
|
+
text: "Secret management is not available. Please ensure the secrets plugin is properly configured.",
|
|
2508
|
+
action: "SET_SECRET"
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
2511
|
+
return { success: false, text: "Secrets service not available" };
|
|
2512
|
+
}
|
|
2513
|
+
const currentState = state ?? await runtime.composeState(message);
|
|
2514
|
+
let extracted;
|
|
2515
|
+
try {
|
|
2516
|
+
const prompt = composePromptFromState({
|
|
2517
|
+
state: currentState,
|
|
2518
|
+
template: extractSecretsTemplate
|
|
2519
|
+
});
|
|
2520
|
+
const result = await runtime.useModel(ModelType.OBJECT_SMALL, {
|
|
2521
|
+
prompt
|
|
2522
|
+
});
|
|
2523
|
+
const secretsArray = Array.isArray(result.secrets) ? result.secrets : [];
|
|
2524
|
+
extracted = {
|
|
2525
|
+
secrets: secretsArray.filter(
|
|
2526
|
+
(s) => s !== null && typeof s === "object"
|
|
2527
|
+
).map((s) => ({
|
|
2528
|
+
key: String(s.key || ""),
|
|
2529
|
+
value: String(s.value || ""),
|
|
2530
|
+
description: s.description ? String(s.description) : void 0,
|
|
2531
|
+
type: s.type
|
|
2532
|
+
})).filter((s) => s.key && s.value),
|
|
2533
|
+
level: result.level
|
|
2534
|
+
};
|
|
2535
|
+
} catch (error) {
|
|
2536
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2537
|
+
logger7.error(`[SetSecret] Failed to extract secrets: ${errorMessage}`);
|
|
2538
|
+
if (callback) {
|
|
2539
|
+
await callback({
|
|
2540
|
+
text: 'I had trouble understanding the secret you wanted to set. Could you please provide it in a clearer format? For example: "Set my OPENAI_API_KEY to sk-..."',
|
|
2541
|
+
action: "SET_SECRET"
|
|
2542
|
+
});
|
|
2543
|
+
}
|
|
2544
|
+
return { success: false, text: "Failed to extract secrets from message" };
|
|
2545
|
+
}
|
|
2546
|
+
if (!extracted.secrets || extracted.secrets.length === 0) {
|
|
2547
|
+
if (callback) {
|
|
2548
|
+
await callback({
|
|
2549
|
+
text: `I couldn't find any secrets to set in your message. Please provide a key and value, like: "Set my OPENAI_API_KEY to sk-..."`,
|
|
2550
|
+
action: "SET_SECRET"
|
|
2551
|
+
});
|
|
2552
|
+
}
|
|
2553
|
+
return { success: false, text: "No secrets found in message" };
|
|
2554
|
+
}
|
|
2555
|
+
const level = extracted.level ?? "global";
|
|
2556
|
+
const context = {
|
|
2557
|
+
level,
|
|
2558
|
+
agentId: runtime.agentId,
|
|
2559
|
+
worldId: level === "world" ? message.roomId : void 0,
|
|
2560
|
+
userId: level === "user" ? message.entityId : void 0,
|
|
2561
|
+
requesterId: message.entityId
|
|
2562
|
+
};
|
|
2563
|
+
const results = [];
|
|
2564
|
+
for (const secret of extracted.secrets) {
|
|
2565
|
+
const key = secret.key.toUpperCase().replace(/[^A-Z0-9_]/g, "_");
|
|
2566
|
+
const validationMethod = inferValidationStrategy(key);
|
|
2567
|
+
try {
|
|
2568
|
+
const success = await secretsService.set(key, secret.value, context, {
|
|
2569
|
+
type: secret.type ?? "secret",
|
|
2570
|
+
description: secret.description ?? `Secret set via conversation`,
|
|
2571
|
+
validationMethod,
|
|
2572
|
+
encrypted: true
|
|
2573
|
+
});
|
|
2574
|
+
results.push({ key, success });
|
|
2575
|
+
if (success) {
|
|
2576
|
+
logger7.info(`[SetSecret] Successfully set secret: ${key}`);
|
|
2577
|
+
}
|
|
2578
|
+
} catch (error) {
|
|
2579
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
2580
|
+
results.push({ key, success: false, error: errorMessage });
|
|
2581
|
+
logger7.error(
|
|
2582
|
+
`[SetSecret] Failed to set secret ${key}: ${errorMessage}`
|
|
2583
|
+
);
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
const successful = results.filter((r) => r.success);
|
|
2587
|
+
const failed = results.filter((r) => !r.success);
|
|
2588
|
+
let responseText;
|
|
2589
|
+
if (successful.length > 0 && failed.length === 0) {
|
|
2590
|
+
const keys = successful.map((r) => r.key).join(", ");
|
|
2591
|
+
responseText = successful.length === 1 ? `I've securely stored your ${keys}. It's now available for use.` : `I've securely stored ${successful.length} secrets: ${keys}. They're now available for use.`;
|
|
2592
|
+
} else if (successful.length === 0 && failed.length > 0) {
|
|
2593
|
+
const errors = failed.map((r) => `${r.key}: ${r.error}`).join("; ");
|
|
2594
|
+
responseText = `I wasn't able to store the secret(s). ${errors}`;
|
|
2595
|
+
} else {
|
|
2596
|
+
const successKeys = successful.map((r) => r.key).join(", ");
|
|
2597
|
+
const failedKeys = failed.map((r) => r.key).join(", ");
|
|
2598
|
+
responseText = `I stored ${successful.length} secret(s) (${successKeys}), but ${failed.length} failed (${failedKeys}).`;
|
|
2599
|
+
}
|
|
2600
|
+
if (callback) {
|
|
2601
|
+
await callback({
|
|
2602
|
+
text: responseText,
|
|
2603
|
+
action: "SET_SECRET"
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
return { success: successful.length > 0, text: responseText };
|
|
2607
|
+
},
|
|
2608
|
+
examples: [
|
|
2609
|
+
[
|
|
2610
|
+
{
|
|
2611
|
+
name: "{{user1}}",
|
|
2612
|
+
content: { text: "Set my OpenAI API key to sk-abc123xyz789" }
|
|
2613
|
+
},
|
|
2614
|
+
{
|
|
2615
|
+
name: "{{agent}}",
|
|
2616
|
+
content: {
|
|
2617
|
+
text: "I've securely stored your OPENAI_API_KEY. It's now available for use.",
|
|
2618
|
+
action: "SET_SECRET"
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
],
|
|
2622
|
+
[
|
|
2623
|
+
{
|
|
2624
|
+
name: "{{user1}}",
|
|
2625
|
+
content: { text: "My Anthropic key is sk-ant-secret123" }
|
|
2626
|
+
},
|
|
2627
|
+
{
|
|
2628
|
+
name: "{{agent}}",
|
|
2629
|
+
content: {
|
|
2630
|
+
text: "I've securely stored your ANTHROPIC_API_KEY. It's now available for use.",
|
|
2631
|
+
action: "SET_SECRET"
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
],
|
|
2635
|
+
[
|
|
2636
|
+
{
|
|
2637
|
+
name: "{{user1}}",
|
|
2638
|
+
content: { text: "Use this Discord bot token: MTIz.abc.xyz" }
|
|
2639
|
+
},
|
|
2640
|
+
{
|
|
2641
|
+
name: "{{agent}}",
|
|
2642
|
+
content: {
|
|
2643
|
+
text: "I've securely stored your DISCORD_BOT_TOKEN. It's now available for use.",
|
|
2644
|
+
action: "SET_SECRET"
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
]
|
|
2648
|
+
]
|
|
2649
|
+
};
|
|
2650
|
+
|
|
2651
|
+
// src/actions/manage-secret.ts
|
|
2652
|
+
import {
|
|
2653
|
+
ModelType as ModelType2,
|
|
2654
|
+
logger as logger8,
|
|
2655
|
+
composePromptFromState as composePromptFromState2
|
|
2656
|
+
} from "@elizaos/core";
|
|
2657
|
+
var extractOperationTemplate = `You are helping manage secrets for an AI agent.
|
|
2658
|
+
|
|
2659
|
+
Determine what operation the user wants to perform:
|
|
2660
|
+
- get: Retrieve a secret value
|
|
2661
|
+
- set: Store a new secret
|
|
2662
|
+
- delete: Remove a secret
|
|
2663
|
+
- list: Show all available secrets (without values)
|
|
2664
|
+
- check: Check if a secret exists
|
|
2665
|
+
|
|
2666
|
+
Common patterns:
|
|
2667
|
+
- "What is my OpenAI key?" -> operation: get, key: OPENAI_API_KEY
|
|
2668
|
+
- "Do I have a Discord token set?" -> operation: check, key: DISCORD_BOT_TOKEN
|
|
2669
|
+
- "Show me my secrets" -> operation: list
|
|
2670
|
+
- "Delete my old API key" -> operation: delete
|
|
2671
|
+
- "Remove TWITTER_API_KEY" -> operation: delete, key: TWITTER_API_KEY
|
|
2672
|
+
- "Set my key to sk-..." -> operation: set, key: <infer>, value: sk-...
|
|
2673
|
+
|
|
2674
|
+
{{recentMessages}}
|
|
2675
|
+
|
|
2676
|
+
Extract the operation, key (if applicable), value (if applicable), and level from the user's message.`;
|
|
2677
|
+
var manageSecretAction = {
|
|
2678
|
+
name: "MANAGE_SECRET",
|
|
2679
|
+
similes: [
|
|
2680
|
+
"SECRET_MANAGEMENT",
|
|
2681
|
+
"HANDLE_SECRET",
|
|
2682
|
+
"SECRET_OPERATION",
|
|
2683
|
+
"GET_SECRET",
|
|
2684
|
+
"DELETE_SECRET",
|
|
2685
|
+
"LIST_SECRETS",
|
|
2686
|
+
"CHECK_SECRET"
|
|
2687
|
+
],
|
|
2688
|
+
description: "Manage secrets - get, set, delete, or list secrets at various levels",
|
|
2689
|
+
validate: async (runtime, message, state) => {
|
|
2690
|
+
const text = message.content.text?.toLowerCase() ?? "";
|
|
2691
|
+
const patterns = [
|
|
2692
|
+
/\b(get|show|what|retrieve)\b.*\b(secret|key|token|credential)/i,
|
|
2693
|
+
/\b(delete|remove|clear)\b.*\b(secret|key|token|credential)/i,
|
|
2694
|
+
/\b(list|show)\b.*\b(secrets|keys|tokens|credentials)/i,
|
|
2695
|
+
/\bdo i have\b.*\b(secret|key|token)/i,
|
|
2696
|
+
/\b(check|is)\b.*\b(secret|key|token)\b.*\b(set|configured)/i,
|
|
2697
|
+
/\bmy secrets\b/i,
|
|
2698
|
+
/\bwhat secrets\b/i
|
|
2699
|
+
];
|
|
2700
|
+
const hasIntent = patterns.some((pattern) => pattern.test(text));
|
|
2701
|
+
if (!hasIntent) {
|
|
2702
|
+
return false;
|
|
2703
|
+
}
|
|
2704
|
+
const secretsService = runtime.getService(SECRETS_SERVICE_TYPE);
|
|
2705
|
+
return secretsService !== null;
|
|
2706
|
+
},
|
|
2707
|
+
handler: async (runtime, message, state, _options, callback) => {
|
|
2708
|
+
logger8.info("[ManageSecret] Processing secret management request");
|
|
2709
|
+
const secretsService = runtime.getService(SECRETS_SERVICE_TYPE);
|
|
2710
|
+
if (!secretsService) {
|
|
2711
|
+
if (callback) {
|
|
2712
|
+
await callback({
|
|
2713
|
+
text: "Secret management is not available.",
|
|
2714
|
+
action: "MANAGE_SECRET"
|
|
2715
|
+
});
|
|
2716
|
+
}
|
|
2717
|
+
return { success: false, text: "Secrets service not available" };
|
|
2718
|
+
}
|
|
2719
|
+
const currentState = state ?? await runtime.composeState(message);
|
|
2720
|
+
let operation;
|
|
2721
|
+
try {
|
|
2722
|
+
const prompt = composePromptFromState2({
|
|
2723
|
+
state: currentState,
|
|
2724
|
+
template: extractOperationTemplate
|
|
2725
|
+
});
|
|
2726
|
+
const result = await runtime.useModel(ModelType2.OBJECT_SMALL, {
|
|
2727
|
+
prompt
|
|
2728
|
+
});
|
|
2729
|
+
operation = {
|
|
2730
|
+
operation: result.operation || "list",
|
|
2731
|
+
key: result.key ? String(result.key) : void 0,
|
|
2732
|
+
value: result.value ? String(result.value) : void 0,
|
|
2733
|
+
level: result.level,
|
|
2734
|
+
description: result.description ? String(result.description) : void 0,
|
|
2735
|
+
type: result.type
|
|
2736
|
+
};
|
|
2737
|
+
} catch (error) {
|
|
2738
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2739
|
+
logger8.error(
|
|
2740
|
+
`[ManageSecret] Failed to extract operation: ${errorMessage}`
|
|
2741
|
+
);
|
|
2742
|
+
if (callback) {
|
|
2743
|
+
await callback({
|
|
2744
|
+
text: "I had trouble understanding what you want to do with secrets. Could you be more specific?",
|
|
2745
|
+
action: "MANAGE_SECRET"
|
|
2746
|
+
});
|
|
2747
|
+
}
|
|
2748
|
+
return {
|
|
2749
|
+
success: false,
|
|
2750
|
+
text: "Failed to extract operation from message"
|
|
2751
|
+
};
|
|
2752
|
+
}
|
|
2753
|
+
const level = operation.level ?? "global";
|
|
2754
|
+
const context = {
|
|
2755
|
+
level,
|
|
2756
|
+
agentId: runtime.agentId,
|
|
2757
|
+
worldId: level === "world" ? message.roomId : void 0,
|
|
2758
|
+
userId: level === "user" ? message.entityId : void 0,
|
|
2759
|
+
requesterId: message.entityId
|
|
2760
|
+
};
|
|
2761
|
+
let responseText;
|
|
2762
|
+
switch (operation.operation) {
|
|
2763
|
+
case "get": {
|
|
2764
|
+
if (!operation.key) {
|
|
2765
|
+
responseText = "Please specify which secret you want to retrieve.";
|
|
2766
|
+
break;
|
|
2767
|
+
}
|
|
2768
|
+
const key = operation.key.toUpperCase().replace(/[^A-Z0-9_]/g, "_");
|
|
2769
|
+
const value = await secretsService.get(key, context);
|
|
2770
|
+
if (value) {
|
|
2771
|
+
const maskedValue = maskSecretValue(value);
|
|
2772
|
+
responseText = `Your ${key} is set to: ${maskedValue}`;
|
|
2773
|
+
} else {
|
|
2774
|
+
responseText = `I don't have a ${key} stored. Would you like to set one?`;
|
|
2775
|
+
}
|
|
2776
|
+
break;
|
|
2777
|
+
}
|
|
2778
|
+
case "set": {
|
|
2779
|
+
if (!operation.key || !operation.value) {
|
|
2780
|
+
responseText = "Please provide both a key and value to set a secret.";
|
|
2781
|
+
break;
|
|
2782
|
+
}
|
|
2783
|
+
const key = operation.key.toUpperCase().replace(/[^A-Z0-9_]/g, "_");
|
|
2784
|
+
try {
|
|
2785
|
+
const success = await secretsService.set(
|
|
2786
|
+
key,
|
|
2787
|
+
operation.value,
|
|
2788
|
+
context,
|
|
2789
|
+
{
|
|
2790
|
+
type: operation.type ?? "secret",
|
|
2791
|
+
description: operation.description ?? "Set via conversation",
|
|
2792
|
+
encrypted: true
|
|
2793
|
+
}
|
|
2794
|
+
);
|
|
2795
|
+
if (success) {
|
|
2796
|
+
responseText = `I've securely stored your ${key}.`;
|
|
2797
|
+
} else {
|
|
2798
|
+
responseText = `Failed to store ${key}. Please try again.`;
|
|
2799
|
+
}
|
|
2800
|
+
} catch (error) {
|
|
2801
|
+
responseText = `Error storing ${key}: ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
2802
|
+
}
|
|
2803
|
+
break;
|
|
2804
|
+
}
|
|
2805
|
+
case "delete": {
|
|
2806
|
+
if (!operation.key) {
|
|
2807
|
+
responseText = "Please specify which secret you want to delete.";
|
|
2808
|
+
break;
|
|
2809
|
+
}
|
|
2810
|
+
const key = operation.key.toUpperCase().replace(/[^A-Z0-9_]/g, "_");
|
|
2811
|
+
const deleted = await secretsService.delete(key, context);
|
|
2812
|
+
if (deleted) {
|
|
2813
|
+
responseText = `I've deleted your ${key}.`;
|
|
2814
|
+
} else {
|
|
2815
|
+
responseText = `I couldn't find a ${key} to delete.`;
|
|
2816
|
+
}
|
|
2817
|
+
break;
|
|
2818
|
+
}
|
|
2819
|
+
case "list": {
|
|
2820
|
+
const metadata = await secretsService.list(context);
|
|
2821
|
+
const keys = Object.keys(metadata);
|
|
2822
|
+
if (keys.length === 0) {
|
|
2823
|
+
responseText = `You don't have any ${level} secrets stored yet.`;
|
|
2824
|
+
} else {
|
|
2825
|
+
const secretList = keys.map((key) => {
|
|
2826
|
+
const config = metadata[key];
|
|
2827
|
+
const status = config.status === "valid" ? "\u2713" : "\u26A0";
|
|
2828
|
+
return `\u2022 ${key} ${status}`;
|
|
2829
|
+
}).join("\n");
|
|
2830
|
+
responseText = `Here are your ${level} secrets:
|
|
2831
|
+
${secretList}`;
|
|
2832
|
+
}
|
|
2833
|
+
break;
|
|
2834
|
+
}
|
|
2835
|
+
case "check": {
|
|
2836
|
+
if (!operation.key) {
|
|
2837
|
+
responseText = "Please specify which secret you want to check.";
|
|
2838
|
+
break;
|
|
2839
|
+
}
|
|
2840
|
+
const key = operation.key.toUpperCase().replace(/[^A-Z0-9_]/g, "_");
|
|
2841
|
+
const exists = await secretsService.exists(key, context);
|
|
2842
|
+
if (exists) {
|
|
2843
|
+
const config = await secretsService.getConfig(key, context);
|
|
2844
|
+
const status = config?.status === "valid" ? "valid" : config?.status ?? "unknown";
|
|
2845
|
+
responseText = `Yes, ${key} is set and its status is: ${status}.`;
|
|
2846
|
+
} else {
|
|
2847
|
+
responseText = `No, ${key} is not set. Would you like to configure it?`;
|
|
2848
|
+
}
|
|
2849
|
+
break;
|
|
2850
|
+
}
|
|
2851
|
+
default:
|
|
2852
|
+
responseText = "I'm not sure what operation you want to perform. You can get, set, delete, list, or check secrets.";
|
|
2853
|
+
}
|
|
2854
|
+
if (callback) {
|
|
2855
|
+
await callback({
|
|
2856
|
+
text: responseText,
|
|
2857
|
+
action: "MANAGE_SECRET"
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2860
|
+
return { success: true, text: responseText };
|
|
2861
|
+
},
|
|
2862
|
+
examples: [
|
|
2863
|
+
[
|
|
2864
|
+
{
|
|
2865
|
+
name: "{{user1}}",
|
|
2866
|
+
content: { text: "What secrets do I have?" }
|
|
2867
|
+
},
|
|
2868
|
+
{
|
|
2869
|
+
name: "{{agent}}",
|
|
2870
|
+
content: {
|
|
2871
|
+
text: "Here are your global secrets:\n\u2022 OPENAI_API_KEY \u2713\n\u2022 ANTHROPIC_API_KEY \u2713",
|
|
2872
|
+
action: "MANAGE_SECRET"
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
],
|
|
2876
|
+
[
|
|
2877
|
+
{
|
|
2878
|
+
name: "{{user1}}",
|
|
2879
|
+
content: { text: "Do I have a Discord token set?" }
|
|
2880
|
+
},
|
|
2881
|
+
{
|
|
2882
|
+
name: "{{agent}}",
|
|
2883
|
+
content: {
|
|
2884
|
+
text: "No, DISCORD_BOT_TOKEN is not set. Would you like to configure it?",
|
|
2885
|
+
action: "MANAGE_SECRET"
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
],
|
|
2889
|
+
[
|
|
2890
|
+
{
|
|
2891
|
+
name: "{{user1}}",
|
|
2892
|
+
content: { text: "Delete my old Twitter API key" }
|
|
2893
|
+
},
|
|
2894
|
+
{
|
|
2895
|
+
name: "{{agent}}",
|
|
2896
|
+
content: {
|
|
2897
|
+
text: "I've deleted your TWITTER_API_KEY.",
|
|
2898
|
+
action: "MANAGE_SECRET"
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
]
|
|
2902
|
+
]
|
|
2903
|
+
};
|
|
2904
|
+
function maskSecretValue(value) {
|
|
2905
|
+
if (value.length <= 8) {
|
|
2906
|
+
return "****";
|
|
2907
|
+
}
|
|
2908
|
+
const visibleStart = value.slice(0, 4);
|
|
2909
|
+
const visibleEnd = value.slice(-4);
|
|
2910
|
+
const maskedLength = Math.min(value.length - 8, 20);
|
|
2911
|
+
const mask = "*".repeat(maskedLength);
|
|
2912
|
+
return `${visibleStart}${mask}${visibleEnd}`;
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
// src/providers/secrets-status.ts
|
|
2916
|
+
import {
|
|
2917
|
+
logger as logger9
|
|
2918
|
+
} from "@elizaos/core";
|
|
2919
|
+
var secretsStatusProvider = {
|
|
2920
|
+
name: "SECRETS_STATUS",
|
|
2921
|
+
description: "Provides information about configured secrets and their status",
|
|
2922
|
+
get: async (runtime, message, state) => {
|
|
2923
|
+
const secretsService = runtime.getService(SECRETS_SERVICE_TYPE);
|
|
2924
|
+
if (!secretsService) {
|
|
2925
|
+
return { text: "" };
|
|
2926
|
+
}
|
|
2927
|
+
try {
|
|
2928
|
+
const globalSecrets = await secretsService.list({
|
|
2929
|
+
level: "global",
|
|
2930
|
+
agentId: runtime.agentId
|
|
2931
|
+
});
|
|
2932
|
+
const secretKeys = Object.keys(globalSecrets);
|
|
2933
|
+
if (secretKeys.length === 0) {
|
|
2934
|
+
return {
|
|
2935
|
+
text: `[Secrets Status]
|
|
2936
|
+
No secrets are currently configured. The agent may need API keys or other credentials to access certain services.`
|
|
2937
|
+
};
|
|
2938
|
+
}
|
|
2939
|
+
const valid = [];
|
|
2940
|
+
const missing = [];
|
|
2941
|
+
const invalid = [];
|
|
2942
|
+
for (const [key, config] of Object.entries(globalSecrets)) {
|
|
2943
|
+
switch (config.status) {
|
|
2944
|
+
case "valid":
|
|
2945
|
+
valid.push(key);
|
|
2946
|
+
break;
|
|
2947
|
+
case "missing":
|
|
2948
|
+
missing.push(key);
|
|
2949
|
+
break;
|
|
2950
|
+
case "invalid":
|
|
2951
|
+
case "expired":
|
|
2952
|
+
case "revoked":
|
|
2953
|
+
invalid.push(key);
|
|
2954
|
+
break;
|
|
2955
|
+
default:
|
|
2956
|
+
valid.push(key);
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
const lines = ["[Secrets Status]"];
|
|
2960
|
+
if (valid.length > 0) {
|
|
2961
|
+
lines.push(`Configured secrets: ${valid.join(", ")}`);
|
|
2962
|
+
}
|
|
2963
|
+
if (invalid.length > 0) {
|
|
2964
|
+
lines.push(`Invalid/expired secrets: ${invalid.join(", ")}`);
|
|
2965
|
+
}
|
|
2966
|
+
if (missing.length > 0) {
|
|
2967
|
+
lines.push(`Missing required secrets: ${missing.join(", ")}`);
|
|
2968
|
+
}
|
|
2969
|
+
const activatorService = runtime.getService(
|
|
2970
|
+
PLUGIN_ACTIVATOR_SERVICE_TYPE
|
|
2971
|
+
);
|
|
2972
|
+
if (activatorService) {
|
|
2973
|
+
const pendingPlugins = activatorService.getPendingPlugins();
|
|
2974
|
+
if (pendingPlugins.length > 0) {
|
|
2975
|
+
lines.push(
|
|
2976
|
+
`Plugins waiting for secrets: ${pendingPlugins.join(", ")}`
|
|
2977
|
+
);
|
|
2978
|
+
const requiredSecrets = activatorService.getRequiredSecrets();
|
|
2979
|
+
if (requiredSecrets.size > 0) {
|
|
2980
|
+
lines.push(
|
|
2981
|
+
`Secrets needed for pending plugins: ${Array.from(requiredSecrets).join(", ")}`
|
|
2982
|
+
);
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
return { text: lines.join("\n") };
|
|
2987
|
+
} catch (error) {
|
|
2988
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2989
|
+
logger9.error(
|
|
2990
|
+
`[SecretsStatusProvider] Error getting secrets status: ${errorMsg}`
|
|
2991
|
+
);
|
|
2992
|
+
return { text: "" };
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
};
|
|
2996
|
+
var secretsInfoProvider = {
|
|
2997
|
+
name: "SECRETS_INFO",
|
|
2998
|
+
description: "Provides detailed secret information based on conversation context",
|
|
2999
|
+
get: async (runtime, message, state) => {
|
|
3000
|
+
const secretsService = runtime.getService(SECRETS_SERVICE_TYPE);
|
|
3001
|
+
if (!secretsService) {
|
|
3002
|
+
return { text: "" };
|
|
3003
|
+
}
|
|
3004
|
+
const text = message.content.text?.toLowerCase() ?? "";
|
|
3005
|
+
const isAboutSecrets = /\b(secret|key|token|credential|api|password|configure)\b/i.test(text);
|
|
3006
|
+
if (!isAboutSecrets) {
|
|
3007
|
+
return { text: "" };
|
|
3008
|
+
}
|
|
3009
|
+
try {
|
|
3010
|
+
const globalSecrets = await secretsService.list({
|
|
3011
|
+
level: "global",
|
|
3012
|
+
agentId: runtime.agentId
|
|
3013
|
+
});
|
|
3014
|
+
const secretCount = Object.keys(globalSecrets).length;
|
|
3015
|
+
if (secretCount === 0) {
|
|
3016
|
+
return {
|
|
3017
|
+
text: `[Secrets Info]
|
|
3018
|
+
No secrets configured. User can set secrets by saying things like "Set my OPENAI_API_KEY to sk-..."`
|
|
3019
|
+
};
|
|
3020
|
+
}
|
|
3021
|
+
const lines = ["[Secrets Info]"];
|
|
3022
|
+
lines.push(`Total configured secrets: ${secretCount}`);
|
|
3023
|
+
const byType = {};
|
|
3024
|
+
for (const [key, config] of Object.entries(globalSecrets)) {
|
|
3025
|
+
const type = config.type ?? "secret";
|
|
3026
|
+
if (!byType[type]) {
|
|
3027
|
+
byType[type] = [];
|
|
3028
|
+
}
|
|
3029
|
+
byType[type].push(key);
|
|
3030
|
+
}
|
|
3031
|
+
for (const [type, keys] of Object.entries(byType)) {
|
|
3032
|
+
lines.push(`${type}: ${keys.join(", ")}`);
|
|
3033
|
+
}
|
|
3034
|
+
const commonSecrets = [
|
|
3035
|
+
"OPENAI_API_KEY",
|
|
3036
|
+
"ANTHROPIC_API_KEY",
|
|
3037
|
+
"DISCORD_BOT_TOKEN",
|
|
3038
|
+
"TELEGRAM_BOT_TOKEN",
|
|
3039
|
+
"TWITTER_API_KEY"
|
|
3040
|
+
];
|
|
3041
|
+
const missingCommon = commonSecrets.filter((key) => !globalSecrets[key]);
|
|
3042
|
+
if (missingCommon.length > 0 && missingCommon.length < commonSecrets.length) {
|
|
3043
|
+
lines.push(`Common secrets not set: ${missingCommon.join(", ")}`);
|
|
3044
|
+
}
|
|
3045
|
+
return { text: lines.join("\n") };
|
|
3046
|
+
} catch (error) {
|
|
3047
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
3048
|
+
logger9.error(`[SecretsInfoProvider] Error: ${errorMsg}`);
|
|
3049
|
+
return { text: "" };
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
};
|
|
3053
|
+
|
|
3054
|
+
// src/onboarding/config.ts
|
|
3055
|
+
var DEFAULT_ONBOARDING_MESSAGES = {
|
|
3056
|
+
welcome: [
|
|
3057
|
+
"Hi! I need to collect some information to get set up. Is now a good time?",
|
|
3058
|
+
"Hey there! I need to configure a few things. Do you have a moment?",
|
|
3059
|
+
"Hello! Could we take a few minutes to get everything set up?"
|
|
3060
|
+
],
|
|
3061
|
+
askSetting: "I need your {{settingName}}. {{usageDescription}}",
|
|
3062
|
+
settingUpdated: "Got it! I've saved your {{settingName}}.",
|
|
3063
|
+
allComplete: "Great! All required settings have been configured. You're all set!",
|
|
3064
|
+
error: "I had trouble understanding that. Could you try again?"
|
|
3065
|
+
};
|
|
3066
|
+
var COMMON_API_KEY_SETTINGS = {
|
|
3067
|
+
OPENAI_API_KEY: {
|
|
3068
|
+
name: "OpenAI API Key",
|
|
3069
|
+
description: "API key for OpenAI services (GPT models)",
|
|
3070
|
+
usageDescription: 'Your OpenAI API key starts with "sk-"',
|
|
3071
|
+
secret: true,
|
|
3072
|
+
public: false,
|
|
3073
|
+
required: false,
|
|
3074
|
+
dependsOn: [],
|
|
3075
|
+
validationMethod: "openai",
|
|
3076
|
+
type: "api_key",
|
|
3077
|
+
envVar: "OPENAI_API_KEY"
|
|
3078
|
+
},
|
|
3079
|
+
ANTHROPIC_API_KEY: {
|
|
3080
|
+
name: "Anthropic API Key",
|
|
3081
|
+
description: "API key for Anthropic services (Claude models)",
|
|
3082
|
+
usageDescription: 'Your Anthropic API key starts with "sk-ant-"',
|
|
3083
|
+
secret: true,
|
|
3084
|
+
public: false,
|
|
3085
|
+
required: false,
|
|
3086
|
+
dependsOn: [],
|
|
3087
|
+
validationMethod: "anthropic",
|
|
3088
|
+
type: "api_key",
|
|
3089
|
+
envVar: "ANTHROPIC_API_KEY"
|
|
3090
|
+
},
|
|
3091
|
+
GROQ_API_KEY: {
|
|
3092
|
+
name: "Groq API Key",
|
|
3093
|
+
description: "API key for Groq inference services",
|
|
3094
|
+
usageDescription: 'Your Groq API key starts with "gsk_"',
|
|
3095
|
+
secret: true,
|
|
3096
|
+
public: false,
|
|
3097
|
+
required: false,
|
|
3098
|
+
dependsOn: [],
|
|
3099
|
+
validationMethod: "groq",
|
|
3100
|
+
type: "api_key",
|
|
3101
|
+
envVar: "GROQ_API_KEY"
|
|
3102
|
+
},
|
|
3103
|
+
GOOGLE_API_KEY: {
|
|
3104
|
+
name: "Google API Key",
|
|
3105
|
+
description: "API key for Google AI services (Gemini)",
|
|
3106
|
+
usageDescription: "Your Google API key for Gemini models",
|
|
3107
|
+
secret: true,
|
|
3108
|
+
public: false,
|
|
3109
|
+
required: false,
|
|
3110
|
+
dependsOn: [],
|
|
3111
|
+
validationMethod: "google",
|
|
3112
|
+
type: "api_key",
|
|
3113
|
+
envVar: "GOOGLE_API_KEY"
|
|
3114
|
+
},
|
|
3115
|
+
DISCORD_TOKEN: {
|
|
3116
|
+
name: "Discord Bot Token",
|
|
3117
|
+
description: "Bot token for Discord integration",
|
|
3118
|
+
usageDescription: "Your Discord bot token from the developer portal",
|
|
3119
|
+
secret: true,
|
|
3120
|
+
public: false,
|
|
3121
|
+
required: false,
|
|
3122
|
+
dependsOn: [],
|
|
3123
|
+
validationMethod: "discord",
|
|
3124
|
+
type: "token",
|
|
3125
|
+
envVar: "DISCORD_TOKEN"
|
|
3126
|
+
},
|
|
3127
|
+
TELEGRAM_BOT_TOKEN: {
|
|
3128
|
+
name: "Telegram Bot Token",
|
|
3129
|
+
description: "Bot token for Telegram integration",
|
|
3130
|
+
usageDescription: "Your Telegram bot token from @BotFather",
|
|
3131
|
+
secret: true,
|
|
3132
|
+
public: false,
|
|
3133
|
+
required: false,
|
|
3134
|
+
dependsOn: [],
|
|
3135
|
+
validationMethod: "telegram",
|
|
3136
|
+
type: "token",
|
|
3137
|
+
envVar: "TELEGRAM_BOT_TOKEN"
|
|
3138
|
+
},
|
|
3139
|
+
TWITTER_USERNAME: {
|
|
3140
|
+
name: "Twitter Username",
|
|
3141
|
+
description: "Twitter/X username for posting",
|
|
3142
|
+
usageDescription: "The Twitter username (without @)",
|
|
3143
|
+
secret: false,
|
|
3144
|
+
public: true,
|
|
3145
|
+
required: false,
|
|
3146
|
+
dependsOn: [],
|
|
3147
|
+
type: "credential"
|
|
3148
|
+
},
|
|
3149
|
+
TWITTER_PASSWORD: {
|
|
3150
|
+
name: "Twitter Password",
|
|
3151
|
+
description: "Twitter/X account password",
|
|
3152
|
+
usageDescription: "The password for your Twitter account",
|
|
3153
|
+
secret: true,
|
|
3154
|
+
public: false,
|
|
3155
|
+
required: false,
|
|
3156
|
+
dependsOn: ["TWITTER_USERNAME"],
|
|
3157
|
+
type: "credential"
|
|
3158
|
+
},
|
|
3159
|
+
TWITTER_EMAIL: {
|
|
3160
|
+
name: "Twitter Email",
|
|
3161
|
+
description: "Email associated with Twitter account",
|
|
3162
|
+
usageDescription: "The email address linked to your Twitter account",
|
|
3163
|
+
secret: false,
|
|
3164
|
+
public: false,
|
|
3165
|
+
required: false,
|
|
3166
|
+
dependsOn: ["TWITTER_USERNAME"],
|
|
3167
|
+
type: "credential",
|
|
3168
|
+
validation: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
|
|
3169
|
+
},
|
|
3170
|
+
TWITTER_2FA_SECRET: {
|
|
3171
|
+
name: "Twitter 2FA Secret",
|
|
3172
|
+
description: "2FA secret for Twitter account",
|
|
3173
|
+
usageDescription: "The 2FA/TOTP secret (if 2FA is enabled)",
|
|
3174
|
+
secret: true,
|
|
3175
|
+
public: false,
|
|
3176
|
+
required: false,
|
|
3177
|
+
dependsOn: ["TWITTER_USERNAME", "TWITTER_PASSWORD"],
|
|
3178
|
+
type: "credential"
|
|
3179
|
+
}
|
|
3180
|
+
};
|
|
3181
|
+
function createOnboardingConfig(requiredKeys, optionalKeys = [], customSettings = {}) {
|
|
3182
|
+
const settings = {};
|
|
3183
|
+
for (const key of requiredKeys) {
|
|
3184
|
+
const common = COMMON_API_KEY_SETTINGS[key] || {};
|
|
3185
|
+
const custom = customSettings[key] || {};
|
|
3186
|
+
settings[key] = {
|
|
3187
|
+
name: custom.name || common.name || key,
|
|
3188
|
+
description: custom.description || common.description || `Configure ${key}`,
|
|
3189
|
+
usageDescription: custom.usageDescription || common.usageDescription,
|
|
3190
|
+
secret: custom.secret ?? common.secret ?? true,
|
|
3191
|
+
public: custom.public ?? common.public ?? false,
|
|
3192
|
+
required: true,
|
|
3193
|
+
dependsOn: custom.dependsOn || common.dependsOn || [],
|
|
3194
|
+
validationMethod: custom.validationMethod || common.validationMethod,
|
|
3195
|
+
type: custom.type || common.type || "api_key",
|
|
3196
|
+
envVar: custom.envVar || common.envVar || key,
|
|
3197
|
+
value: null,
|
|
3198
|
+
...custom
|
|
3199
|
+
};
|
|
3200
|
+
}
|
|
3201
|
+
for (const key of optionalKeys) {
|
|
3202
|
+
const common = COMMON_API_KEY_SETTINGS[key] || {};
|
|
3203
|
+
const custom = customSettings[key] || {};
|
|
3204
|
+
settings[key] = {
|
|
3205
|
+
name: custom.name || common.name || key,
|
|
3206
|
+
description: custom.description || common.description || `Configure ${key}`,
|
|
3207
|
+
usageDescription: custom.usageDescription || common.usageDescription,
|
|
3208
|
+
secret: custom.secret ?? common.secret ?? true,
|
|
3209
|
+
public: custom.public ?? common.public ?? false,
|
|
3210
|
+
required: false,
|
|
3211
|
+
dependsOn: custom.dependsOn || common.dependsOn || [],
|
|
3212
|
+
validationMethod: custom.validationMethod || common.validationMethod,
|
|
3213
|
+
type: custom.type || common.type || "api_key",
|
|
3214
|
+
envVar: custom.envVar || common.envVar || key,
|
|
3215
|
+
value: null,
|
|
3216
|
+
...custom
|
|
3217
|
+
};
|
|
3218
|
+
}
|
|
3219
|
+
return { settings };
|
|
3220
|
+
}
|
|
3221
|
+
function getUnconfiguredRequired(config) {
|
|
3222
|
+
return Object.entries(config.settings).filter(
|
|
3223
|
+
([_, setting]) => setting.required && setting.value === null
|
|
3224
|
+
);
|
|
3225
|
+
}
|
|
3226
|
+
function getUnconfiguredOptional(config) {
|
|
3227
|
+
return Object.entries(config.settings).filter(
|
|
3228
|
+
([_, setting]) => !setting.required && setting.value === null
|
|
3229
|
+
);
|
|
3230
|
+
}
|
|
3231
|
+
function isOnboardingComplete(config) {
|
|
3232
|
+
return getUnconfiguredRequired(config).length === 0;
|
|
3233
|
+
}
|
|
3234
|
+
function getNextSetting(config) {
|
|
3235
|
+
const unconfigured = getUnconfiguredRequired(config);
|
|
3236
|
+
for (const [key, setting] of unconfigured) {
|
|
3237
|
+
const dependenciesMet = setting.dependsOn.every((dep) => {
|
|
3238
|
+
const depSetting = config.settings[dep];
|
|
3239
|
+
return depSetting && depSetting.value !== null;
|
|
3240
|
+
});
|
|
3241
|
+
const isVisible = !setting.visibleIf || setting.visibleIf(config.settings);
|
|
3242
|
+
if (dependenciesMet && isVisible) {
|
|
3243
|
+
return [key, setting];
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
const optionalUnconfigured = getUnconfiguredOptional(config);
|
|
3247
|
+
for (const [key, setting] of optionalUnconfigured) {
|
|
3248
|
+
const dependenciesMet = setting.dependsOn.every((dep) => {
|
|
3249
|
+
const depSetting = config.settings[dep];
|
|
3250
|
+
return depSetting && depSetting.value !== null;
|
|
3251
|
+
});
|
|
3252
|
+
const isVisible = !setting.visibleIf || setting.visibleIf(config.settings);
|
|
3253
|
+
if (dependenciesMet && isVisible) {
|
|
3254
|
+
return [key, setting];
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
return null;
|
|
3258
|
+
}
|
|
3259
|
+
function generateSettingPrompt(key, setting, agentName) {
|
|
3260
|
+
const required = setting.required ? "(Required)" : "(Optional)";
|
|
3261
|
+
const usage = setting.usageDescription || setting.description;
|
|
3262
|
+
return `${agentName} needs to collect the ${setting.name} ${required}.
|
|
3263
|
+
Description: ${usage}
|
|
3264
|
+
Ask the user for their ${setting.name} in a natural, conversational way.`;
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
// src/onboarding/service.ts
|
|
3268
|
+
import { Service as Service3, logger as logger10 } from "@elizaos/core";
|
|
3269
|
+
var ONBOARDING_SERVICE_TYPE = "SECRETS_ONBOARDING";
|
|
3270
|
+
var OnboardingService = class _OnboardingService extends Service3 {
|
|
3271
|
+
static serviceType = ONBOARDING_SERVICE_TYPE;
|
|
3272
|
+
capabilityDescription = "Manage secrets onboarding across chat platforms";
|
|
3273
|
+
secretsService = null;
|
|
3274
|
+
sessions = /* @__PURE__ */ new Map();
|
|
3275
|
+
constructor(runtime) {
|
|
3276
|
+
super(runtime);
|
|
3277
|
+
}
|
|
3278
|
+
/**
|
|
3279
|
+
* Start the service
|
|
3280
|
+
*/
|
|
3281
|
+
static async start(runtime) {
|
|
3282
|
+
const service = new _OnboardingService(runtime);
|
|
3283
|
+
await service.initialize();
|
|
3284
|
+
return service;
|
|
3285
|
+
}
|
|
3286
|
+
/**
|
|
3287
|
+
* Initialize the service
|
|
3288
|
+
*/
|
|
3289
|
+
async initialize() {
|
|
3290
|
+
logger10.info("[OnboardingService] Starting");
|
|
3291
|
+
this.secretsService = this.runtime.getService("SECRETS");
|
|
3292
|
+
this.registerEvents();
|
|
3293
|
+
logger10.info("[OnboardingService] Started");
|
|
3294
|
+
}
|
|
3295
|
+
async stop() {
|
|
3296
|
+
logger10.info("[OnboardingService] Stopping");
|
|
3297
|
+
this.sessions.clear();
|
|
3298
|
+
logger10.info("[OnboardingService] Stopped");
|
|
3299
|
+
}
|
|
3300
|
+
/**
|
|
3301
|
+
* Register platform-specific event handlers.
|
|
3302
|
+
*/
|
|
3303
|
+
registerEvents() {
|
|
3304
|
+
this.runtime.registerEvent("DISCORD_WORLD_JOINED", async (params) => {
|
|
3305
|
+
const server = params.server;
|
|
3306
|
+
if (server) {
|
|
3307
|
+
logger10.info(`[OnboardingService] Discord world joined: ${server.id}`);
|
|
3308
|
+
}
|
|
3309
|
+
});
|
|
3310
|
+
this.runtime.registerEvent("DISCORD_SERVER_CONNECTED", async (params) => {
|
|
3311
|
+
const server = params.server;
|
|
3312
|
+
if (server) {
|
|
3313
|
+
logger10.info(
|
|
3314
|
+
`[OnboardingService] Discord server connected: ${server.id}`
|
|
3315
|
+
);
|
|
3316
|
+
}
|
|
3317
|
+
});
|
|
3318
|
+
this.runtime.registerEvent("TELEGRAM_WORLD_JOINED", async (params) => {
|
|
3319
|
+
const typedParams = params;
|
|
3320
|
+
if (typedParams.world && typedParams.chat && typedParams.entities && typedParams.botUsername) {
|
|
3321
|
+
logger10.info(
|
|
3322
|
+
`[OnboardingService] Telegram world joined: ${typedParams.world.id}`
|
|
3323
|
+
);
|
|
3324
|
+
await this.startTelegramOnboarding(
|
|
3325
|
+
typedParams.world,
|
|
3326
|
+
typedParams.chat,
|
|
3327
|
+
typedParams.entities,
|
|
3328
|
+
typedParams.botUsername
|
|
3329
|
+
);
|
|
3330
|
+
}
|
|
3331
|
+
});
|
|
3332
|
+
}
|
|
3333
|
+
/**
|
|
3334
|
+
* Initialize onboarding for a world with the given config.
|
|
3335
|
+
*/
|
|
3336
|
+
async initializeOnboarding(world, config) {
|
|
3337
|
+
logger10.info(
|
|
3338
|
+
`[OnboardingService] Initializing onboarding for world: ${world.id}`
|
|
3339
|
+
);
|
|
3340
|
+
if (!world.metadata) {
|
|
3341
|
+
world.metadata = {};
|
|
3342
|
+
}
|
|
3343
|
+
const settingsState = {};
|
|
3344
|
+
for (const [key, setting] of Object.entries(config.settings)) {
|
|
3345
|
+
settingsState[key] = {
|
|
3346
|
+
...setting,
|
|
3347
|
+
value: null
|
|
3348
|
+
};
|
|
3349
|
+
}
|
|
3350
|
+
const metadata = world.metadata;
|
|
3351
|
+
metadata["settings"] = settingsState;
|
|
3352
|
+
metadata.onboardingConfig = config;
|
|
3353
|
+
await this.runtime.updateWorld(world);
|
|
3354
|
+
logger10.info(
|
|
3355
|
+
`[OnboardingService] Onboarding initialized for world: ${world.id}`
|
|
3356
|
+
);
|
|
3357
|
+
}
|
|
3358
|
+
/**
|
|
3359
|
+
* Start onboarding via DM (Discord).
|
|
3360
|
+
*/
|
|
3361
|
+
async startDiscordOnboardingDM(serverId, ownerId, worldId, config) {
|
|
3362
|
+
const messages = config.messages?.welcome || DEFAULT_ONBOARDING_MESSAGES.welcome;
|
|
3363
|
+
const _randomMessage = messages[Math.floor(Math.random() * messages.length)];
|
|
3364
|
+
logger10.info(
|
|
3365
|
+
`[OnboardingService] Discord DM onboarding started - server: ${serverId}, owner: ${ownerId}, world: ${worldId}`
|
|
3366
|
+
);
|
|
3367
|
+
}
|
|
3368
|
+
/**
|
|
3369
|
+
* Start onboarding via deep link (Telegram).
|
|
3370
|
+
*/
|
|
3371
|
+
async startTelegramOnboarding(world, chat, entities, botUsername) {
|
|
3372
|
+
let ownerId = null;
|
|
3373
|
+
let ownerUsername = null;
|
|
3374
|
+
for (const entity of entities) {
|
|
3375
|
+
if (entity.metadata?.telegram?.adminTitle === "Owner") {
|
|
3376
|
+
ownerId = entity.metadata.telegram.id;
|
|
3377
|
+
ownerUsername = entity.metadata.telegram.username;
|
|
3378
|
+
break;
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
if (!ownerId) {
|
|
3382
|
+
logger10.warn("[OnboardingService] No owner found for Telegram group");
|
|
3383
|
+
return;
|
|
3384
|
+
}
|
|
3385
|
+
const telegramService = this.runtime.getService("telegram");
|
|
3386
|
+
if (telegramService?.messageManager) {
|
|
3387
|
+
const deepLinkMessage = [
|
|
3388
|
+
`Hello @${ownerUsername}! Could we take a few minutes to get everything set up?`,
|
|
3389
|
+
`Please click this link to start chatting with me: https://t.me/${botUsername}?start=onboarding`
|
|
3390
|
+
].join(" ");
|
|
3391
|
+
await telegramService.messageManager.sendMessage(chat.id, {
|
|
3392
|
+
text: deepLinkMessage
|
|
3393
|
+
});
|
|
3394
|
+
logger10.info(
|
|
3395
|
+
`[OnboardingService] Sent Telegram deep link - chatId: ${chat.id}, ownerId: ${ownerId}`
|
|
3396
|
+
);
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
/**
|
|
3400
|
+
* Start a new onboarding session.
|
|
3401
|
+
*/
|
|
3402
|
+
async startSession(worldId, userId, roomId, config, platform = "other", mode = "conversational") {
|
|
3403
|
+
const session = {
|
|
3404
|
+
worldId,
|
|
3405
|
+
userId,
|
|
3406
|
+
roomId,
|
|
3407
|
+
config,
|
|
3408
|
+
currentSettingKey: null,
|
|
3409
|
+
startedAt: Date.now(),
|
|
3410
|
+
lastActivityAt: Date.now(),
|
|
3411
|
+
platform,
|
|
3412
|
+
mode
|
|
3413
|
+
};
|
|
3414
|
+
this.sessions.set(roomId, session);
|
|
3415
|
+
logger10.info(
|
|
3416
|
+
`[OnboardingService] Session started - roomId: ${roomId}, worldId: ${worldId}, userId: ${userId}`
|
|
3417
|
+
);
|
|
3418
|
+
return session;
|
|
3419
|
+
}
|
|
3420
|
+
/**
|
|
3421
|
+
* Get an active session by room ID.
|
|
3422
|
+
*/
|
|
3423
|
+
getSession(roomId) {
|
|
3424
|
+
return this.sessions.get(roomId) || null;
|
|
3425
|
+
}
|
|
3426
|
+
/**
|
|
3427
|
+
* Process a user message during onboarding.
|
|
3428
|
+
*/
|
|
3429
|
+
async processMessage(roomId, message) {
|
|
3430
|
+
const session = this.sessions.get(roomId);
|
|
3431
|
+
if (!session) {
|
|
3432
|
+
return { shouldRespond: false };
|
|
3433
|
+
}
|
|
3434
|
+
session.lastActivityAt = Date.now();
|
|
3435
|
+
const unconfigured = getUnconfiguredRequired(session.config);
|
|
3436
|
+
if (unconfigured.length === 0) {
|
|
3437
|
+
return {
|
|
3438
|
+
shouldRespond: true,
|
|
3439
|
+
response: session.config.messages?.allComplete || DEFAULT_ONBOARDING_MESSAGES.allComplete,
|
|
3440
|
+
complete: true
|
|
3441
|
+
};
|
|
3442
|
+
}
|
|
3443
|
+
const text = message.content?.text || "";
|
|
3444
|
+
const currentSetting = session.currentSettingKey ? session.config.settings[session.currentSettingKey] : null;
|
|
3445
|
+
if (currentSetting && text.trim()) {
|
|
3446
|
+
const value = text.trim();
|
|
3447
|
+
if (currentSetting.validation && !currentSetting.validation(value)) {
|
|
3448
|
+
return {
|
|
3449
|
+
shouldRespond: true,
|
|
3450
|
+
response: `That doesn't look like a valid ${currentSetting.name}. ${currentSetting.usageDescription || "Please try again."}`
|
|
3451
|
+
};
|
|
3452
|
+
}
|
|
3453
|
+
if (this.secretsService) {
|
|
3454
|
+
const context = {
|
|
3455
|
+
level: "world",
|
|
3456
|
+
agentId: this.runtime.agentId,
|
|
3457
|
+
worldId: session.worldId,
|
|
3458
|
+
userId: session.userId
|
|
3459
|
+
};
|
|
3460
|
+
await this.secretsService.set(
|
|
3461
|
+
session.currentSettingKey,
|
|
3462
|
+
value,
|
|
3463
|
+
context,
|
|
3464
|
+
{
|
|
3465
|
+
description: currentSetting.description,
|
|
3466
|
+
type: currentSetting.type,
|
|
3467
|
+
encrypted: currentSetting.secret
|
|
3468
|
+
}
|
|
3469
|
+
);
|
|
3470
|
+
}
|
|
3471
|
+
session.config.settings[session.currentSettingKey].value = value;
|
|
3472
|
+
if (isOnboardingComplete(session.config)) {
|
|
3473
|
+
this.sessions.delete(roomId);
|
|
3474
|
+
return {
|
|
3475
|
+
shouldRespond: true,
|
|
3476
|
+
response: session.config.messages?.allComplete || DEFAULT_ONBOARDING_MESSAGES.allComplete,
|
|
3477
|
+
updatedKey: session.currentSettingKey,
|
|
3478
|
+
complete: true
|
|
3479
|
+
};
|
|
3480
|
+
}
|
|
3481
|
+
const next2 = getNextSetting(session.config);
|
|
3482
|
+
if (next2) {
|
|
3483
|
+
const [nextKey, nextSetting] = next2;
|
|
3484
|
+
session.currentSettingKey = nextKey;
|
|
3485
|
+
const askMessage = (session.config.messages?.askSetting || DEFAULT_ONBOARDING_MESSAGES.askSetting).replace("{{settingName}}", nextSetting.name).replace(
|
|
3486
|
+
"{{usageDescription}}",
|
|
3487
|
+
nextSetting.usageDescription || nextSetting.description
|
|
3488
|
+
);
|
|
3489
|
+
return {
|
|
3490
|
+
shouldRespond: true,
|
|
3491
|
+
response: `${session.config.messages?.settingUpdated || DEFAULT_ONBOARDING_MESSAGES.settingUpdated.replace("{{settingName}}", currentSetting.name)}
|
|
3492
|
+
|
|
3493
|
+
${askMessage}`,
|
|
3494
|
+
updatedKey: session.currentSettingKey
|
|
3495
|
+
};
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
const next = getNextSetting(session.config);
|
|
3499
|
+
if (next) {
|
|
3500
|
+
const [nextKey, nextSetting] = next;
|
|
3501
|
+
session.currentSettingKey = nextKey;
|
|
3502
|
+
const askMessage = (session.config.messages?.askSetting || DEFAULT_ONBOARDING_MESSAGES.askSetting).replace("{{settingName}}", nextSetting.name).replace(
|
|
3503
|
+
"{{usageDescription}}",
|
|
3504
|
+
nextSetting.usageDescription || nextSetting.description
|
|
3505
|
+
);
|
|
3506
|
+
return {
|
|
3507
|
+
shouldRespond: true,
|
|
3508
|
+
response: askMessage
|
|
3509
|
+
};
|
|
3510
|
+
}
|
|
3511
|
+
return { shouldRespond: false };
|
|
3512
|
+
}
|
|
3513
|
+
/**
|
|
3514
|
+
* End an onboarding session.
|
|
3515
|
+
*/
|
|
3516
|
+
endSession(roomId) {
|
|
3517
|
+
this.sessions.delete(roomId);
|
|
3518
|
+
logger10.info(`[OnboardingService] Session ended - roomId: ${roomId}`);
|
|
3519
|
+
}
|
|
3520
|
+
/**
|
|
3521
|
+
* Get the onboarding status for a world.
|
|
3522
|
+
*/
|
|
3523
|
+
async getOnboardingStatus(worldId) {
|
|
3524
|
+
const world = await this.runtime.getWorld(worldId);
|
|
3525
|
+
if (!world?.metadata?.settings) {
|
|
3526
|
+
return {
|
|
3527
|
+
initialized: false,
|
|
3528
|
+
complete: false,
|
|
3529
|
+
configuredCount: 0,
|
|
3530
|
+
requiredCount: 0,
|
|
3531
|
+
missingRequired: []
|
|
3532
|
+
};
|
|
3533
|
+
}
|
|
3534
|
+
const settings = world.metadata.settings;
|
|
3535
|
+
const entries = Object.entries(settings);
|
|
3536
|
+
const required = entries.filter(([_, s]) => s.required);
|
|
3537
|
+
const configured = required.filter(([_, s]) => s.value !== null);
|
|
3538
|
+
const missing = required.filter(([_, s]) => s.value === null).map(([k, _]) => k);
|
|
3539
|
+
return {
|
|
3540
|
+
initialized: true,
|
|
3541
|
+
complete: missing.length === 0,
|
|
3542
|
+
configuredCount: configured.length,
|
|
3543
|
+
requiredCount: required.length,
|
|
3544
|
+
missingRequired: missing
|
|
3545
|
+
};
|
|
3546
|
+
}
|
|
3547
|
+
/**
|
|
3548
|
+
* Generate the SETTINGS provider context for LLM.
|
|
3549
|
+
*/
|
|
3550
|
+
generateSettingsContext(config, isOnboarding, agentName) {
|
|
3551
|
+
const entries = Object.entries(config.settings);
|
|
3552
|
+
const unconfigured = getUnconfiguredRequired(config);
|
|
3553
|
+
const settingsList = entries.map(([key, setting]) => {
|
|
3554
|
+
const status = setting.value !== null ? "Configured" : "Not set";
|
|
3555
|
+
const required = setting.required ? "(Required)" : "(Optional)";
|
|
3556
|
+
const value = setting.secret && setting.value ? "****************" : setting.value || "Not set";
|
|
3557
|
+
return `${key}: ${value} ${required}
|
|
3558
|
+
(${setting.name}) ${setting.usageDescription || setting.description}`;
|
|
3559
|
+
}).join("\n\n");
|
|
3560
|
+
const validKeys = `Valid setting keys: ${entries.map(([k, _]) => k).join(", ")}`;
|
|
3561
|
+
if (isOnboarding && unconfigured.length > 0) {
|
|
3562
|
+
return `# PRIORITY TASK: Onboarding
|
|
3563
|
+
|
|
3564
|
+
${agentName} needs to help the user configure ${unconfigured.length} required settings:
|
|
3565
|
+
|
|
3566
|
+
${settingsList}
|
|
3567
|
+
|
|
3568
|
+
${validKeys}
|
|
3569
|
+
|
|
3570
|
+
Instructions for ${agentName}:
|
|
3571
|
+
- Only update settings if the user is clearly responding to a setting you are currently asking about.
|
|
3572
|
+
- If the user's reply clearly maps to a setting and a valid value, you **must** call the UPDATE_SETTINGS action.
|
|
3573
|
+
- Never hallucinate settings or respond with values not listed above.
|
|
3574
|
+
- Prioritize configuring required settings before optional ones.`;
|
|
3575
|
+
}
|
|
3576
|
+
return `## Current Configuration
|
|
3577
|
+
${unconfigured.length > 0 ? `IMPORTANT: ${unconfigured.length} required settings still need configuration.
|
|
3578
|
+
|
|
3579
|
+
` : "All required settings are configured.\n\n"}${settingsList}`;
|
|
3580
|
+
}
|
|
3581
|
+
};
|
|
3582
|
+
|
|
3583
|
+
// src/onboarding/provider.ts
|
|
3584
|
+
import { ChannelType as ChannelType2, logger as logger11 } from "@elizaos/core";
|
|
3585
|
+
function formatSettingValue(setting, isOnboarding) {
|
|
3586
|
+
if (setting.value === null || setting.value === void 0) {
|
|
3587
|
+
return "Not set";
|
|
3588
|
+
}
|
|
3589
|
+
if (setting.secret && !isOnboarding) {
|
|
3590
|
+
return "****************";
|
|
3591
|
+
}
|
|
3592
|
+
return String(setting.value);
|
|
3593
|
+
}
|
|
3594
|
+
function generateStatusMessage(settings, isOnboarding, agentName, senderName) {
|
|
3595
|
+
const entries = Object.entries(settings);
|
|
3596
|
+
const formattedSettings = entries.map(([key, setting]) => {
|
|
3597
|
+
if (setting.visibleIf && !setting.visibleIf(settings)) {
|
|
3598
|
+
return null;
|
|
3599
|
+
}
|
|
3600
|
+
return {
|
|
3601
|
+
key,
|
|
3602
|
+
name: setting.name,
|
|
3603
|
+
value: formatSettingValue(setting, isOnboarding),
|
|
3604
|
+
description: setting.description,
|
|
3605
|
+
usageDescription: setting.usageDescription || setting.description,
|
|
3606
|
+
required: setting.required,
|
|
3607
|
+
configured: setting.value !== null
|
|
3608
|
+
};
|
|
3609
|
+
}).filter(Boolean);
|
|
3610
|
+
const requiredUnconfigured = formattedSettings.filter(
|
|
3611
|
+
(s) => s?.required && !s.configured
|
|
3612
|
+
).length;
|
|
3613
|
+
if (isOnboarding) {
|
|
3614
|
+
const settingsList = formattedSettings.map((s) => {
|
|
3615
|
+
if (!s) return "";
|
|
3616
|
+
const label = s.required ? "(Required)" : "(Optional)";
|
|
3617
|
+
return `${s.key}: ${s.value} ${label}
|
|
3618
|
+
(${s.name}) ${s.usageDescription}`;
|
|
3619
|
+
}).filter(Boolean).join("\n\n");
|
|
3620
|
+
const validKeys = `Valid setting keys: ${entries.map(([k]) => k).join(", ")}`;
|
|
3621
|
+
const instructions = `Instructions for ${agentName}:
|
|
3622
|
+
- Only update settings if the user is clearly responding to a setting you are currently asking about.
|
|
3623
|
+
- If the user's reply clearly maps to a setting and a valid value, you **must** call the UPDATE_SETTINGS action with the correct key and value.
|
|
3624
|
+
- Never hallucinate settings or respond with values not listed above.
|
|
3625
|
+
- Do not call UPDATE_SETTINGS just because onboarding started. Only update when the user provides a specific value.
|
|
3626
|
+
- Answer setting-related questions using only the name, description, and value from the list.`;
|
|
3627
|
+
if (requiredUnconfigured > 0) {
|
|
3628
|
+
const name = senderName || "user";
|
|
3629
|
+
return `# PRIORITY TASK: Onboarding with ${name}
|
|
3630
|
+
|
|
3631
|
+
${agentName} needs to help the user configure ${requiredUnconfigured} required settings:
|
|
3632
|
+
|
|
3633
|
+
${settingsList}
|
|
3634
|
+
|
|
3635
|
+
${validKeys}
|
|
3636
|
+
|
|
3637
|
+
${instructions}
|
|
3638
|
+
|
|
3639
|
+
- Prioritize configuring required settings before optional ones.`;
|
|
3640
|
+
}
|
|
3641
|
+
return `All required settings have been configured. Here's the current configuration:
|
|
3642
|
+
|
|
3643
|
+
${settingsList}
|
|
3644
|
+
|
|
3645
|
+
${validKeys}
|
|
3646
|
+
|
|
3647
|
+
${instructions}`;
|
|
3648
|
+
}
|
|
3649
|
+
return `## Current Configuration
|
|
3650
|
+
|
|
3651
|
+
${requiredUnconfigured > 0 ? `IMPORTANT!: ${requiredUnconfigured} required settings still need configuration. ${agentName} should get onboarded with the OWNER as soon as possible.
|
|
3652
|
+
|
|
3653
|
+
` : "All required settings are configured.\n\n"}${formattedSettings.map((s) => {
|
|
3654
|
+
if (!s) return "";
|
|
3655
|
+
return `### ${s.name}
|
|
3656
|
+
**Value:** ${s.value}
|
|
3657
|
+
**Description:** ${s.description}`;
|
|
3658
|
+
}).filter(Boolean).join("\n\n")}`;
|
|
3659
|
+
}
|
|
3660
|
+
var onboardingSettingsProvider = {
|
|
3661
|
+
name: "ONBOARDING_SETTINGS",
|
|
3662
|
+
description: "Current onboarding settings status for secrets collection",
|
|
3663
|
+
get: async (runtime, message, state) => {
|
|
3664
|
+
const room = await runtime.getRoom(message.roomId);
|
|
3665
|
+
if (!room) {
|
|
3666
|
+
logger11.debug("[OnboardingSettingsProvider] No room found");
|
|
3667
|
+
return {
|
|
3668
|
+
data: { settings: [] },
|
|
3669
|
+
values: { settings: "Error: Room not found" },
|
|
3670
|
+
text: "Error: Room not found"
|
|
3671
|
+
};
|
|
3672
|
+
}
|
|
3673
|
+
if (!room.worldId) {
|
|
3674
|
+
logger11.debug("[OnboardingSettingsProvider] No world ID for room");
|
|
3675
|
+
return {
|
|
3676
|
+
data: { settings: [] },
|
|
3677
|
+
values: { settings: "Room has no associated world." },
|
|
3678
|
+
text: "Room has no associated world."
|
|
3679
|
+
};
|
|
3680
|
+
}
|
|
3681
|
+
const isOnboarding = room.type === ChannelType2.DM;
|
|
3682
|
+
const world = await runtime.getWorld(room.worldId);
|
|
3683
|
+
if (!world) {
|
|
3684
|
+
logger11.debug("[OnboardingSettingsProvider] No world found");
|
|
3685
|
+
return {
|
|
3686
|
+
data: { settings: [] },
|
|
3687
|
+
values: { settings: "Error: World not found" },
|
|
3688
|
+
text: "Error: World not found"
|
|
3689
|
+
};
|
|
3690
|
+
}
|
|
3691
|
+
const worldSettings = world.metadata?.settings;
|
|
3692
|
+
if (!worldSettings) {
|
|
3693
|
+
if (isOnboarding) {
|
|
3694
|
+
return {
|
|
3695
|
+
data: { settings: [] },
|
|
3696
|
+
values: {
|
|
3697
|
+
settings: "No settings configured for this world. Use initializeOnboarding to set up."
|
|
3698
|
+
},
|
|
3699
|
+
text: "No settings configured for this world."
|
|
3700
|
+
};
|
|
3701
|
+
}
|
|
3702
|
+
return {
|
|
3703
|
+
data: { settings: [] },
|
|
3704
|
+
values: { settings: "" },
|
|
3705
|
+
text: ""
|
|
3706
|
+
};
|
|
3707
|
+
}
|
|
3708
|
+
const agentName = runtime.character.name ?? "Agent";
|
|
3709
|
+
const senderName = state?.senderName;
|
|
3710
|
+
const output = generateStatusMessage(
|
|
3711
|
+
worldSettings,
|
|
3712
|
+
isOnboarding,
|
|
3713
|
+
agentName,
|
|
3714
|
+
senderName
|
|
3715
|
+
);
|
|
3716
|
+
return {
|
|
3717
|
+
data: { settings: worldSettings },
|
|
3718
|
+
values: { settings: output },
|
|
3719
|
+
text: output
|
|
3720
|
+
};
|
|
3721
|
+
}
|
|
3722
|
+
};
|
|
3723
|
+
var missingSecretsProvider = {
|
|
3724
|
+
name: "MISSING_SECRETS",
|
|
3725
|
+
description: "Lists secrets that still need to be configured",
|
|
3726
|
+
get: async (runtime, message, _state) => {
|
|
3727
|
+
const room = await runtime.getRoom(message.roomId);
|
|
3728
|
+
if (!room?.worldId) {
|
|
3729
|
+
return {
|
|
3730
|
+
data: { missing: [] },
|
|
3731
|
+
values: { missingSecrets: "" },
|
|
3732
|
+
text: ""
|
|
3733
|
+
};
|
|
3734
|
+
}
|
|
3735
|
+
const world = await runtime.getWorld(room.worldId);
|
|
3736
|
+
if (!world?.metadata?.settings) {
|
|
3737
|
+
return {
|
|
3738
|
+
data: { missing: [] },
|
|
3739
|
+
values: { missingSecrets: "" },
|
|
3740
|
+
text: ""
|
|
3741
|
+
};
|
|
3742
|
+
}
|
|
3743
|
+
const settings = world.metadata.settings;
|
|
3744
|
+
const entries = Object.entries(settings);
|
|
3745
|
+
const missingRequired = entries.filter(([_, s]) => s.required && s.value === null).map(([key, setting]) => ({
|
|
3746
|
+
key,
|
|
3747
|
+
name: setting.name,
|
|
3748
|
+
description: setting.usageDescription || setting.description
|
|
3749
|
+
}));
|
|
3750
|
+
const missingOptional = entries.filter(([_, s]) => !s.required && s.value === null).map(([key, setting]) => ({
|
|
3751
|
+
key,
|
|
3752
|
+
name: setting.name,
|
|
3753
|
+
description: setting.usageDescription || setting.description
|
|
3754
|
+
}));
|
|
3755
|
+
if (missingRequired.length === 0 && missingOptional.length === 0) {
|
|
3756
|
+
return {
|
|
3757
|
+
data: { missing: [] },
|
|
3758
|
+
values: { missingSecrets: "All secrets are configured." },
|
|
3759
|
+
text: "All secrets are configured."
|
|
3760
|
+
};
|
|
3761
|
+
}
|
|
3762
|
+
let output = "";
|
|
3763
|
+
if (missingRequired.length > 0) {
|
|
3764
|
+
output += `Missing required secrets:
|
|
3765
|
+
${missingRequired.map((s) => `- ${s.key}: ${s.description}`).join("\n")}
|
|
3766
|
+
|
|
3767
|
+
`;
|
|
3768
|
+
}
|
|
3769
|
+
if (missingOptional.length > 0) {
|
|
3770
|
+
output += `Missing optional secrets:
|
|
3771
|
+
${missingOptional.map((s) => `- ${s.key}: ${s.description}`).join("\n")}`;
|
|
3772
|
+
}
|
|
3773
|
+
return {
|
|
3774
|
+
data: {
|
|
3775
|
+
missing: [...missingRequired, ...missingOptional],
|
|
3776
|
+
missingRequired,
|
|
3777
|
+
missingOptional
|
|
3778
|
+
},
|
|
3779
|
+
values: { missingSecrets: output.trim() },
|
|
3780
|
+
text: output.trim()
|
|
3781
|
+
};
|
|
3782
|
+
}
|
|
3783
|
+
};
|
|
3784
|
+
|
|
3785
|
+
// src/onboarding/action.ts
|
|
3786
|
+
import { ChannelType as ChannelType3, ModelType as ModelType3, logger as logger12 } from "@elizaos/core";
|
|
3787
|
+
async function extractSettingValues(runtime, message, state, settings) {
|
|
3788
|
+
const unconfigured = Object.entries(settings).filter(
|
|
3789
|
+
([_, s]) => s.value === null
|
|
3790
|
+
);
|
|
3791
|
+
if (unconfigured.length === 0) {
|
|
3792
|
+
return [];
|
|
3793
|
+
}
|
|
3794
|
+
const settingsContext = unconfigured.map(([key, setting]) => {
|
|
3795
|
+
const requiredStr = setting.required ? "Required." : "Optional.";
|
|
3796
|
+
return `${key}: ${setting.description} ${requiredStr}`;
|
|
3797
|
+
}).join("\n");
|
|
3798
|
+
const prompt = `I need to extract settings values from the user's message.
|
|
3799
|
+
|
|
3800
|
+
Available settings:
|
|
3801
|
+
${settingsContext}
|
|
3802
|
+
|
|
3803
|
+
User message: ${state.text || message.content?.text || ""}
|
|
3804
|
+
|
|
3805
|
+
For each setting mentioned in the user's message, extract the value.
|
|
3806
|
+
|
|
3807
|
+
Only return settings that are clearly mentioned in the user's message.
|
|
3808
|
+
If a setting is mentioned but no clear value is provided, do not include it.`;
|
|
3809
|
+
const result = await runtime.useModel(ModelType3.OBJECT_LARGE, {
|
|
3810
|
+
prompt,
|
|
3811
|
+
output: "array",
|
|
3812
|
+
schema: {
|
|
3813
|
+
type: "array",
|
|
3814
|
+
items: {
|
|
3815
|
+
type: "object",
|
|
3816
|
+
properties: {
|
|
3817
|
+
key: { type: "string" },
|
|
3818
|
+
value: { type: "string" }
|
|
3819
|
+
},
|
|
3820
|
+
required: ["key", "value"]
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
});
|
|
3824
|
+
if (!result) {
|
|
3825
|
+
return [];
|
|
3826
|
+
}
|
|
3827
|
+
const validUpdates = [];
|
|
3828
|
+
const extractFromResult = (obj) => {
|
|
3829
|
+
if (Array.isArray(obj)) {
|
|
3830
|
+
for (const item of obj) {
|
|
3831
|
+
extractFromResult(item);
|
|
3832
|
+
}
|
|
3833
|
+
} else if (typeof obj === "object" && obj !== null) {
|
|
3834
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
3835
|
+
if (settings[key] && typeof value !== "object") {
|
|
3836
|
+
validUpdates.push({ key, value });
|
|
3837
|
+
} else {
|
|
3838
|
+
extractFromResult(value);
|
|
3839
|
+
}
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
};
|
|
3843
|
+
extractFromResult(result);
|
|
3844
|
+
return validUpdates;
|
|
3845
|
+
}
|
|
3846
|
+
async function processSettingUpdates(runtime, world, settings, updates, secretsService) {
|
|
3847
|
+
if (!updates.length) {
|
|
3848
|
+
return { updatedAny: false, messages: [] };
|
|
3849
|
+
}
|
|
3850
|
+
const messages = [];
|
|
3851
|
+
let updatedAny = false;
|
|
3852
|
+
const updatedSettings = { ...settings };
|
|
3853
|
+
for (const update of updates) {
|
|
3854
|
+
const setting = updatedSettings[update.key];
|
|
3855
|
+
if (!setting) continue;
|
|
3856
|
+
if (setting.dependsOn?.length) {
|
|
3857
|
+
const dependenciesMet = setting.dependsOn.every((dep) => {
|
|
3858
|
+
const depSetting = updatedSettings[dep];
|
|
3859
|
+
return depSetting && depSetting.value !== null;
|
|
3860
|
+
});
|
|
3861
|
+
if (!dependenciesMet) {
|
|
3862
|
+
messages.push(`Cannot update ${setting.name} - dependencies not met`);
|
|
3863
|
+
continue;
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
const valueStr = String(update.value);
|
|
3867
|
+
if (setting.validation && !setting.validation(valueStr)) {
|
|
3868
|
+
messages.push(`Invalid value for ${setting.name}`);
|
|
3869
|
+
continue;
|
|
3870
|
+
}
|
|
3871
|
+
if (setting.validationMethod) {
|
|
3872
|
+
const validation = await validateSecret(
|
|
3873
|
+
update.key,
|
|
3874
|
+
valueStr,
|
|
3875
|
+
setting.validationMethod
|
|
3876
|
+
);
|
|
3877
|
+
if (!validation.isValid) {
|
|
3878
|
+
messages.push(
|
|
3879
|
+
`Validation failed for ${setting.name}: ${validation.error}`
|
|
3880
|
+
);
|
|
3881
|
+
continue;
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
updatedSettings[update.key] = {
|
|
3885
|
+
...setting,
|
|
3886
|
+
value: valueStr
|
|
3887
|
+
};
|
|
3888
|
+
if (secretsService) {
|
|
3889
|
+
const context = {
|
|
3890
|
+
level: "world",
|
|
3891
|
+
agentId: runtime.agentId,
|
|
3892
|
+
worldId: world.id
|
|
3893
|
+
};
|
|
3894
|
+
await secretsService.set(update.key, valueStr, context, {
|
|
3895
|
+
description: setting.description,
|
|
3896
|
+
type: setting.type,
|
|
3897
|
+
encrypted: setting.secret
|
|
3898
|
+
});
|
|
3899
|
+
}
|
|
3900
|
+
messages.push(`Updated ${setting.name} successfully`);
|
|
3901
|
+
updatedAny = true;
|
|
3902
|
+
if (setting.onSetAction) {
|
|
3903
|
+
const actionMessage = setting.onSetAction(update.value);
|
|
3904
|
+
if (actionMessage) {
|
|
3905
|
+
messages.push(actionMessage);
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
if (updatedAny) {
|
|
3910
|
+
if (!world.metadata) {
|
|
3911
|
+
world.metadata = {};
|
|
3912
|
+
}
|
|
3913
|
+
world.metadata["settings"] = updatedSettings;
|
|
3914
|
+
await runtime.updateWorld(world);
|
|
3915
|
+
}
|
|
3916
|
+
return { updatedAny, messages };
|
|
3917
|
+
}
|
|
3918
|
+
function getNextRequiredSetting(settings) {
|
|
3919
|
+
const entries = Object.entries(settings);
|
|
3920
|
+
for (const [key, setting] of entries) {
|
|
3921
|
+
if (!setting.required || setting.value !== null) continue;
|
|
3922
|
+
const dependenciesMet = (setting.dependsOn || []).every((dep) => {
|
|
3923
|
+
const depSetting = settings[dep];
|
|
3924
|
+
return depSetting && depSetting.value !== null;
|
|
3925
|
+
});
|
|
3926
|
+
if (dependenciesMet) {
|
|
3927
|
+
return [key, setting];
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
return null;
|
|
3931
|
+
}
|
|
3932
|
+
function countUnconfiguredRequired(settings) {
|
|
3933
|
+
return Object.values(settings).filter((s) => s.required && s.value === null).length;
|
|
3934
|
+
}
|
|
3935
|
+
var updateSettingsAction = {
|
|
3936
|
+
name: "UPDATE_SETTINGS",
|
|
3937
|
+
similes: ["UPDATE_SETTING", "SAVE_SETTING", "SET_CONFIGURATION", "CONFIGURE"],
|
|
3938
|
+
description: "Saves a configuration setting during the onboarding process. Use when onboarding with a world owner or admin.",
|
|
3939
|
+
validate: async (runtime, message, _state) => {
|
|
3940
|
+
if (message.content.channelType !== ChannelType3.DM) {
|
|
3941
|
+
return false;
|
|
3942
|
+
}
|
|
3943
|
+
const room = await runtime.getRoom(message.roomId);
|
|
3944
|
+
if (!room?.worldId) return false;
|
|
3945
|
+
const world = await runtime.getWorld(room.worldId);
|
|
3946
|
+
if (!world?.metadata?.settings) return false;
|
|
3947
|
+
const settings = world.metadata.settings;
|
|
3948
|
+
const hasUnconfigured = Object.values(settings).some(
|
|
3949
|
+
(s) => s.value === null
|
|
3950
|
+
);
|
|
3951
|
+
return hasUnconfigured;
|
|
3952
|
+
},
|
|
3953
|
+
handler: async (runtime, message, state, _options, callback) => {
|
|
3954
|
+
if (!state || !callback) {
|
|
3955
|
+
return {
|
|
3956
|
+
text: "State and callback required",
|
|
3957
|
+
values: { success: false },
|
|
3958
|
+
data: { actionName: "UPDATE_SETTINGS" },
|
|
3959
|
+
success: false
|
|
3960
|
+
};
|
|
3961
|
+
}
|
|
3962
|
+
const room = await runtime.getRoom(message.roomId);
|
|
3963
|
+
if (!room?.worldId) {
|
|
3964
|
+
await callback({ text: "Unable to find room configuration." });
|
|
3965
|
+
return {
|
|
3966
|
+
text: "Room not found",
|
|
3967
|
+
values: { success: false },
|
|
3968
|
+
data: { actionName: "UPDATE_SETTINGS" },
|
|
3969
|
+
success: false
|
|
3970
|
+
};
|
|
3971
|
+
}
|
|
3972
|
+
const world = await runtime.getWorld(room.worldId);
|
|
3973
|
+
if (!world?.metadata?.settings) {
|
|
3974
|
+
await callback({ text: "No settings configured for this world." });
|
|
3975
|
+
return {
|
|
3976
|
+
text: "No settings found",
|
|
3977
|
+
values: { success: false },
|
|
3978
|
+
data: { actionName: "UPDATE_SETTINGS" },
|
|
3979
|
+
success: false
|
|
3980
|
+
};
|
|
3981
|
+
}
|
|
3982
|
+
const settings = world.metadata.settings;
|
|
3983
|
+
const secretsService = runtime.getService(
|
|
3984
|
+
"SECRETS"
|
|
3985
|
+
);
|
|
3986
|
+
logger12.info("[UpdateSettings] Extracting settings from message");
|
|
3987
|
+
const extractedSettings = await extractSettingValues(
|
|
3988
|
+
runtime,
|
|
3989
|
+
message,
|
|
3990
|
+
state,
|
|
3991
|
+
settings
|
|
3992
|
+
);
|
|
3993
|
+
logger12.info(
|
|
3994
|
+
`[UpdateSettings] Extracted ${extractedSettings.length} settings`
|
|
3995
|
+
);
|
|
3996
|
+
const results = await processSettingUpdates(
|
|
3997
|
+
runtime,
|
|
3998
|
+
world,
|
|
3999
|
+
settings,
|
|
4000
|
+
extractedSettings,
|
|
4001
|
+
secretsService
|
|
4002
|
+
);
|
|
4003
|
+
const updatedWorld = await runtime.getWorld(room.worldId);
|
|
4004
|
+
const updatedSettings = updatedWorld?.metadata?.settings;
|
|
4005
|
+
if (results.updatedAny) {
|
|
4006
|
+
const remaining = countUnconfiguredRequired(updatedSettings || settings);
|
|
4007
|
+
if (remaining === 0) {
|
|
4008
|
+
await callback({
|
|
4009
|
+
text: `${results.messages.join("\n")}
|
|
4010
|
+
|
|
4011
|
+
All required settings have been configured! You're all set.`,
|
|
4012
|
+
actions: ["ONBOARDING_COMPLETE"]
|
|
4013
|
+
});
|
|
4014
|
+
return {
|
|
4015
|
+
text: "Onboarding complete",
|
|
4016
|
+
values: { success: true, onboardingComplete: true },
|
|
4017
|
+
data: {
|
|
4018
|
+
actionName: "UPDATE_SETTINGS",
|
|
4019
|
+
action: "ONBOARDING_COMPLETE"
|
|
4020
|
+
},
|
|
4021
|
+
success: true
|
|
4022
|
+
};
|
|
4023
|
+
}
|
|
4024
|
+
const next2 = getNextRequiredSetting(updatedSettings || settings);
|
|
4025
|
+
const nextPrompt = next2 ? `
|
|
4026
|
+
|
|
4027
|
+
Next, I need your ${next2[1].name}. ${next2[1].usageDescription || next2[1].description}` : "";
|
|
4028
|
+
await callback({
|
|
4029
|
+
text: `${results.messages.join("\n")}${nextPrompt}`,
|
|
4030
|
+
actions: ["SETTING_UPDATED"]
|
|
4031
|
+
});
|
|
4032
|
+
return {
|
|
4033
|
+
text: "Settings updated",
|
|
4034
|
+
values: { success: true, remainingRequired: remaining },
|
|
4035
|
+
data: {
|
|
4036
|
+
actionName: "UPDATE_SETTINGS",
|
|
4037
|
+
action: "SETTING_UPDATED",
|
|
4038
|
+
updated: extractedSettings.map((s) => s.key)
|
|
4039
|
+
},
|
|
4040
|
+
success: true
|
|
4041
|
+
};
|
|
4042
|
+
}
|
|
4043
|
+
const next = getNextRequiredSetting(settings);
|
|
4044
|
+
const prompt = next ? `I couldn't understand that. I need your ${next[1].name}. ${next[1].usageDescription || next[1].description}` : "I couldn't extract any settings from your message. Could you try again?";
|
|
4045
|
+
await callback({
|
|
4046
|
+
text: prompt,
|
|
4047
|
+
actions: ["SETTING_UPDATE_FAILED"]
|
|
4048
|
+
});
|
|
4049
|
+
return {
|
|
4050
|
+
text: "No settings updated",
|
|
4051
|
+
values: { success: false },
|
|
4052
|
+
data: { actionName: "UPDATE_SETTINGS", action: "SETTING_UPDATE_FAILED" },
|
|
4053
|
+
success: false
|
|
4054
|
+
};
|
|
4055
|
+
},
|
|
4056
|
+
examples: [
|
|
4057
|
+
[
|
|
4058
|
+
{
|
|
4059
|
+
name: "{{name1}}",
|
|
4060
|
+
content: {
|
|
4061
|
+
text: "My OpenAI key is sk-abc123def456",
|
|
4062
|
+
source: "discord"
|
|
4063
|
+
}
|
|
4064
|
+
},
|
|
4065
|
+
{
|
|
4066
|
+
name: "{{name2}}",
|
|
4067
|
+
content: {
|
|
4068
|
+
text: "Got it! I've saved your OpenAI API Key. Next, I need your Anthropic API Key.",
|
|
4069
|
+
actions: ["SETTING_UPDATED"],
|
|
4070
|
+
source: "discord"
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
],
|
|
4074
|
+
[
|
|
4075
|
+
{
|
|
4076
|
+
name: "{{name1}}",
|
|
4077
|
+
content: {
|
|
4078
|
+
text: "Here's my Twitter login: @myhandle with password secret123",
|
|
4079
|
+
source: "discord"
|
|
4080
|
+
}
|
|
4081
|
+
},
|
|
4082
|
+
{
|
|
4083
|
+
name: "{{name2}}",
|
|
4084
|
+
content: {
|
|
4085
|
+
text: "Perfect! I've updated your Twitter Username and Twitter Password. We're all set!",
|
|
4086
|
+
actions: ["ONBOARDING_COMPLETE"],
|
|
4087
|
+
source: "discord"
|
|
4088
|
+
}
|
|
4089
|
+
}
|
|
4090
|
+
]
|
|
4091
|
+
]
|
|
4092
|
+
};
|
|
4093
|
+
|
|
4094
|
+
// src/plugin.ts
|
|
4095
|
+
var secretsManagerPlugin = {
|
|
4096
|
+
name: "@elizaos/plugin-secrets-manager",
|
|
4097
|
+
description: "Multi-level secret management with encryption, dynamic plugin activation, and conversational onboarding",
|
|
4098
|
+
// Services
|
|
4099
|
+
services: [SecretsService, PluginActivatorService, OnboardingService],
|
|
4100
|
+
// Actions for natural language secret management and onboarding
|
|
4101
|
+
actions: [setSecretAction, manageSecretAction, updateSettingsAction],
|
|
4102
|
+
// Providers for context injection
|
|
4103
|
+
providers: [
|
|
4104
|
+
secretsStatusProvider,
|
|
4105
|
+
secretsInfoProvider,
|
|
4106
|
+
onboardingSettingsProvider,
|
|
4107
|
+
missingSecretsProvider
|
|
4108
|
+
],
|
|
4109
|
+
// Plugin initialization
|
|
4110
|
+
init: async (config, runtime) => {
|
|
4111
|
+
logger13.info("[SecretsManagerPlugin] Initializing");
|
|
4112
|
+
logger13.info("[SecretsManagerPlugin] Initialized");
|
|
4113
|
+
}
|
|
4114
|
+
};
|
|
4115
|
+
export {
|
|
4116
|
+
BaseSecretStorage,
|
|
4117
|
+
COMMON_API_KEY_SETTINGS,
|
|
4118
|
+
CharacterSettingsStorage,
|
|
4119
|
+
ComponentSecretStorage,
|
|
4120
|
+
CompositeSecretStorage,
|
|
4121
|
+
DEFAULT_ONBOARDING_MESSAGES,
|
|
4122
|
+
EncryptionError,
|
|
4123
|
+
KeyManager,
|
|
4124
|
+
MAX_ACCESS_LOG_ENTRIES,
|
|
4125
|
+
MemorySecretStorage,
|
|
4126
|
+
ONBOARDING_SERVICE_TYPE,
|
|
4127
|
+
OnboardingService,
|
|
4128
|
+
PLUGIN_ACTIVATOR_SERVICE_TYPE,
|
|
4129
|
+
PermissionDeniedError,
|
|
4130
|
+
PluginActivatorService,
|
|
4131
|
+
SECRETS_SERVICE_TYPE,
|
|
4132
|
+
SECRET_DESCRIPTION_MAX_LENGTH,
|
|
4133
|
+
SECRET_KEY_MAX_LENGTH,
|
|
4134
|
+
SECRET_KEY_PATTERN,
|
|
4135
|
+
SECRET_VALUE_MAX_LENGTH,
|
|
4136
|
+
SecretNotFoundError,
|
|
4137
|
+
SecretsError,
|
|
4138
|
+
SecretsService,
|
|
4139
|
+
StorageError,
|
|
4140
|
+
ValidationError,
|
|
4141
|
+
ValidationStrategies,
|
|
4142
|
+
WorldMetadataStorage,
|
|
4143
|
+
createOnboardingConfig,
|
|
4144
|
+
decrypt,
|
|
4145
|
+
secretsManagerPlugin as default,
|
|
4146
|
+
deriveKeyFromAgentId,
|
|
4147
|
+
deriveKeyPbkdf2,
|
|
4148
|
+
encrypt,
|
|
4149
|
+
generateKey,
|
|
4150
|
+
generateSalt,
|
|
4151
|
+
generateSecureToken,
|
|
4152
|
+
generateSettingPrompt,
|
|
4153
|
+
getNextSetting,
|
|
4154
|
+
getUnconfiguredOptional,
|
|
4155
|
+
getUnconfiguredRequired,
|
|
4156
|
+
inferValidationStrategy,
|
|
4157
|
+
isEncryptedSecret,
|
|
4158
|
+
isOnboardingComplete,
|
|
4159
|
+
manageSecretAction,
|
|
4160
|
+
missingSecretsProvider,
|
|
4161
|
+
onboardingSettingsProvider,
|
|
4162
|
+
registerValidator,
|
|
4163
|
+
secretsInfoProvider,
|
|
4164
|
+
secretsManagerPlugin,
|
|
4165
|
+
secretsStatusProvider,
|
|
4166
|
+
setSecretAction,
|
|
4167
|
+
unregisterValidator,
|
|
4168
|
+
updateSettingsAction,
|
|
4169
|
+
validateSecret
|
|
4170
|
+
};
|
|
4171
|
+
//# sourceMappingURL=index.js.map
|