@@ -8,12 +8,15 @@ import {
8
8
Content ,
9
9
ActionExample ,
10
10
generateObject ,
11
+ elizaLogger ,
11
12
} from "@elizaos/core" ;
12
13
import { Indexer , ZgFile , getFlowContract } from "@0glabs/0g-ts-sdk" ;
13
14
import { ethers } from "ethers" ;
14
15
import { composeContext } from "@elizaos/core" ;
15
16
import { promises as fs } from "fs" ;
16
-
17
+ import { FileSecurityValidator } from "../utils/security" ;
18
+ import { logSecurityEvent , monitorUpload , monitorFileValidation , monitorCleanup } from '../utils/monitoring' ;
19
+ import path from 'path' ;
17
20
import { uploadTemplate } from "../templates/upload" ;
18
21
19
22
export interface UploadContent extends Content {
@@ -24,7 +27,7 @@ function isUploadContent(
24
27
_runtime : IAgentRuntime ,
25
28
content : any
26
29
) : content is UploadContent {
27
- console . log ( "Content for upload", content ) ;
30
+ elizaLogger . debug ( "Validating upload content ", { content } ) ;
28
31
return typeof content . filePath === "string" ;
29
32
}
30
33
@@ -41,103 +44,435 @@ export const zgUpload: Action = {
41
44
] ,
42
45
description : "Store data using 0G protocol" ,
43
46
validate : async ( runtime : IAgentRuntime , message : Memory ) => {
44
- const zgIndexerRpc = ! ! runtime . getSetting ( "ZEROG_INDEXER_RPC" ) ;
45
- const zgEvmRpc = ! ! runtime . getSetting ( "ZEROG_EVM_RPC" ) ;
46
- const zgPrivateKey = ! ! runtime . getSetting ( "ZEROG_PRIVATE_KEY" ) ;
47
- const flowAddr = ! ! runtime . getSetting ( "ZEROG_FLOW_ADDRESS" ) ;
48
- return zgIndexerRpc && zgEvmRpc && zgPrivateKey && flowAddr ;
47
+ elizaLogger . debug ( "Starting ZG_UPLOAD validation" , { messageId : message . id } ) ;
48
+
49
+ try {
50
+ const settings = {
51
+ indexerRpc : runtime . getSetting ( "ZEROG_INDEXER_RPC" ) ,
52
+ evmRpc : runtime . getSetting ( "ZEROG_EVM_RPC" ) ,
53
+ privateKey : runtime . getSetting ( "ZEROG_PRIVATE_KEY" ) ,
54
+ flowAddr : runtime . getSetting ( "ZEROG_FLOW_ADDRESS" )
55
+ } ;
56
+
57
+ elizaLogger . debug ( "Checking ZeroG settings" , {
58
+ hasIndexerRpc : Boolean ( settings . indexerRpc ) ,
59
+ hasEvmRpc : Boolean ( settings . evmRpc ) ,
60
+ hasPrivateKey : Boolean ( settings . privateKey ) ,
61
+ hasFlowAddr : Boolean ( settings . flowAddr )
62
+ } ) ;
63
+
64
+ const hasRequiredSettings = Object . entries ( settings ) . every ( ( [ key , value ] ) => Boolean ( value ) ) ;
65
+
66
+ if ( ! hasRequiredSettings ) {
67
+ const missingSettings = Object . entries ( settings )
68
+ . filter ( ( [ _ , value ] ) => ! value )
69
+ . map ( ( [ key ] ) => key ) ;
70
+
71
+ elizaLogger . error ( "Missing required ZeroG settings" , {
72
+ missingSettings,
73
+ messageId : message . id
74
+ } ) ;
75
+ return false ;
76
+ }
77
+
78
+ const config = {
79
+ maxFileSize : parseInt ( runtime . getSetting ( "ZEROG_MAX_FILE_SIZE" ) || "10485760" ) ,
80
+ allowedExtensions : runtime . getSetting ( "ZEROG_ALLOWED_EXTENSIONS" ) ?. split ( "," ) || [ ".pdf" , ".png" , ".jpg" , ".jpeg" , ".doc" , ".docx" ] ,
81
+ uploadDirectory : runtime . getSetting ( "ZEROG_UPLOAD_DIR" ) || "/tmp/zerog-uploads" ,
82
+ enableVirusScan : runtime . getSetting ( "ZEROG_ENABLE_VIRUS_SCAN" ) === "true"
83
+ } ;
84
+
85
+ // Validate config values
86
+ if ( isNaN ( config . maxFileSize ) || config . maxFileSize <= 0 ) {
87
+ elizaLogger . error ( "Invalid ZEROG_MAX_FILE_SIZE setting" , {
88
+ value : runtime . getSetting ( "ZEROG_MAX_FILE_SIZE" ) ,
89
+ messageId : message . id
90
+ } ) ;
91
+ return false ;
92
+ }
93
+
94
+ if ( ! config . allowedExtensions || config . allowedExtensions . length === 0 ) {
95
+ elizaLogger . error ( "Invalid ZEROG_ALLOWED_EXTENSIONS setting" , {
96
+ value : runtime . getSetting ( "ZEROG_ALLOWED_EXTENSIONS" ) ,
97
+ messageId : message . id
98
+ } ) ;
99
+ return false ;
100
+ }
101
+
102
+ elizaLogger . info ( "ZG_UPLOAD action settings validated" , {
103
+ config,
104
+ messageId : message . id
105
+ } ) ;
106
+ return true ;
107
+ } catch ( error ) {
108
+ elizaLogger . error ( "Error validating ZG_UPLOAD settings" , {
109
+ error : error instanceof Error ? error . message : String ( error ) ,
110
+ stack : error instanceof Error ? error . stack : undefined ,
111
+ messageId : message . id
112
+ } ) ;
113
+ return false ;
114
+ }
49
115
} ,
116
+
50
117
handler : async (
51
118
runtime : IAgentRuntime ,
52
119
message : Memory ,
53
120
state : State ,
54
121
_options : any ,
55
122
callback : HandlerCallback
56
123
) => {
57
- console . log ( "ZG_UPLOAD action called" ) ;
58
- if ( ! state ) {
59
- state = ( await runtime . composeState ( message ) ) as State ;
60
- } else {
61
- state = await runtime . updateRecentMessageState ( state ) ;
62
- }
63
-
64
- // Compose upload context
65
- const uploadContext = composeContext ( {
66
- state,
67
- template : uploadTemplate ,
124
+ elizaLogger . info ( "ZG_UPLOAD action started" , {
125
+ messageId : message . id ,
126
+ hasState : Boolean ( state ) ,
127
+ hasCallback : Boolean ( callback )
68
128
} ) ;
69
129
70
- // Generate upload content
71
- const content = await generateObject ( {
72
- runtime,
73
- context : uploadContext ,
74
- modelClass : ModelClass . LARGE ,
75
- } ) ;
130
+ let file : ZgFile | undefined ;
131
+ let cleanupRequired = false ;
76
132
77
- // Validate upload content
78
- if ( ! isUploadContent ( runtime , content ) ) {
79
- console . error ( "Invalid content for UPLOAD action." ) ;
80
- if ( callback ) {
81
- callback ( {
82
- text : "Unable to process 0G upload request. Invalid content provided." ,
83
- content : { error : "Invalid upload content" } ,
133
+ try {
134
+ // Update state if needed
135
+ if ( ! state ) {
136
+ elizaLogger . debug ( "No state provided, composing new state" ) ;
137
+ state = ( await runtime . composeState ( message ) ) as State ;
138
+ } else {
139
+ elizaLogger . debug ( "Updating existing state" ) ;
140
+ state = await runtime . updateRecentMessageState ( state ) ;
141
+ }
142
+
143
+ // Compose upload context
144
+ elizaLogger . debug ( "Composing upload context" ) ;
145
+ const uploadContext = composeContext ( {
146
+ state,
147
+ template : uploadTemplate ,
148
+ } ) ;
149
+
150
+ // Generate upload content
151
+ elizaLogger . debug ( "Generating upload content" ) ;
152
+ const content = await generateObject ( {
153
+ runtime,
154
+ context : uploadContext ,
155
+ modelClass : ModelClass . LARGE ,
156
+ } ) ;
157
+
158
+ // Validate upload content
159
+ if ( ! isUploadContent ( runtime , content ) ) {
160
+ const error = "Invalid content for UPLOAD action" ;
161
+ elizaLogger . error ( error , {
162
+ content,
163
+ messageId : message . id
84
164
} ) ;
165
+ if ( callback ) {
166
+ callback ( {
167
+ text : "Unable to process 0G upload request. Invalid content provided." ,
168
+ content : { error }
169
+ } ) ;
170
+ }
171
+ return false ;
85
172
}
86
- return false ;
87
- }
88
173
89
- try {
90
- const zgIndexerRpc = runtime . getSetting ( "ZEROG_INDEXER_RPC" ) ;
91
- const zgEvmRpc = runtime . getSetting ( "ZEROG_EVM_RPC" ) ;
92
- const zgPrivateKey = runtime . getSetting ( "ZEROG_PRIVATE_KEY" ) ;
93
- const flowAddr = runtime . getSetting ( "ZEROG_FLOW_ADDRESS" ) ;
94
174
const filePath = content . filePath ;
175
+ elizaLogger . debug ( "Extracted file path" , { filePath, content } ) ;
176
+
95
177
if ( ! filePath ) {
96
- console . error ( "File path is required" ) ;
178
+ const error = "File path is required" ;
179
+ elizaLogger . error ( error , { messageId : message . id } ) ;
180
+ if ( callback ) {
181
+ callback ( {
182
+ text : "File path is required for upload." ,
183
+ content : { error }
184
+ } ) ;
185
+ }
97
186
return false ;
98
187
}
99
188
100
- // Check if file exists and is accessible
189
+ // Initialize security validator
190
+ const securityConfig = {
191
+ maxFileSize : parseInt ( runtime . getSetting ( "ZEROG_MAX_FILE_SIZE" ) || "10485760" ) ,
192
+ allowedExtensions : runtime . getSetting ( "ZEROG_ALLOWED_EXTENSIONS" ) ?. split ( "," ) || [ ".pdf" , ".png" , ".jpg" , ".jpeg" , ".doc" , ".docx" ] ,
193
+ uploadDirectory : runtime . getSetting ( "ZEROG_UPLOAD_DIR" ) || "/tmp/zerog-uploads" ,
194
+ enableVirusScan : runtime . getSetting ( "ZEROG_ENABLE_VIRUS_SCAN" ) === "true"
195
+ } ;
196
+
197
+ let validator : FileSecurityValidator ;
101
198
try {
102
- await fs . access ( filePath ) ;
199
+ elizaLogger . debug ( "Initializing security validator" , {
200
+ config : securityConfig ,
201
+ messageId : message . id
202
+ } ) ;
203
+ validator = new FileSecurityValidator ( securityConfig ) ;
103
204
} catch ( error ) {
104
- console . error (
105
- `File ${ filePath } does not exist or is not accessible:` ,
106
- error
107
- ) ;
205
+ const errorMessage = `Security validator initialization failed: ${ error instanceof Error ? error . message : String ( error ) } ` ;
206
+ elizaLogger . error ( errorMessage , {
207
+ config : securityConfig ,
208
+ messageId : message . id
209
+ } ) ;
210
+ if ( callback ) {
211
+ callback ( {
212
+ text : "Upload failed: Security configuration error." ,
213
+ content : { error : errorMessage }
214
+ } ) ;
215
+ }
108
216
return false ;
109
217
}
110
218
111
- const file = await ZgFile . fromFilePath ( filePath ) ;
112
- var [ tree , err ] = await file . merkleTree ( ) ;
113
- if ( err === null ) {
114
- console . log ( "File Root Hash: " , tree . rootHash ( ) ) ;
115
- } else {
116
- console . log ( "Error getting file root hash: " , err ) ;
219
+ // Validate file type
220
+ elizaLogger . debug ( "Starting file type validation" , { filePath } ) ;
221
+ const typeValidation = await validator . validateFileType ( filePath ) ;
222
+ monitorFileValidation ( filePath , "file_type" , typeValidation . isValid , {
223
+ error : typeValidation . error
224
+ } ) ;
225
+ if ( ! typeValidation . isValid ) {
226
+ const error = "File type validation failed" ;
227
+ elizaLogger . error ( error , {
228
+ error : typeValidation . error ,
229
+ filePath,
230
+ messageId : message . id
231
+ } ) ;
232
+ if ( callback ) {
233
+ callback ( {
234
+ text : `Upload failed: ${ typeValidation . error } ` ,
235
+ content : { error : typeValidation . error }
236
+ } ) ;
237
+ }
117
238
return false ;
118
239
}
119
240
120
- const provider = new ethers . JsonRpcProvider ( zgEvmRpc ) ;
121
- const signer = new ethers . Wallet ( zgPrivateKey , provider ) ;
122
- const indexer = new Indexer ( zgIndexerRpc ) ;
123
- const flowContract = getFlowContract ( flowAddr , signer ) ;
124
-
125
- var [ tx , err ] = await indexer . upload (
126
- file ,
127
- 0 ,
128
- zgEvmRpc ,
129
- flowContract
130
- ) ;
131
- if ( err === null ) {
132
- console . log ( "File uploaded successfully, tx: " , tx ) ;
133
- } else {
134
- console . error ( "Error uploading file: " , err ) ;
241
+ // Validate file size
242
+ elizaLogger . debug ( "Starting file size validation" , { filePath } ) ;
243
+ const sizeValidation = await validator . validateFileSize ( filePath ) ;
244
+ monitorFileValidation ( filePath , "file_size" , sizeValidation . isValid , {
245
+ error : sizeValidation . error
246
+ } ) ;
247
+ if ( ! sizeValidation . isValid ) {
248
+ const error = "File size validation failed" ;
249
+ elizaLogger . error ( error , {
250
+ error : sizeValidation . error ,
251
+ filePath,
252
+ messageId : message . id
253
+ } ) ;
254
+ if ( callback ) {
255
+ callback ( {
256
+ text : `Upload failed: ${ sizeValidation . error } ` ,
257
+ content : { error : sizeValidation . error }
258
+ } ) ;
259
+ }
260
+ return false ;
261
+ }
262
+
263
+ // Validate file path
264
+ elizaLogger . debug ( "Starting file path validation" , { filePath } ) ;
265
+ const pathValidation = await validator . validateFilePath ( filePath ) ;
266
+ monitorFileValidation ( filePath , "file_path" , pathValidation . isValid , {
267
+ error : pathValidation . error
268
+ } ) ;
269
+ if ( ! pathValidation . isValid ) {
270
+ const error = "File path validation failed" ;
271
+ elizaLogger . error ( error , {
272
+ error : pathValidation . error ,
273
+ filePath,
274
+ messageId : message . id
275
+ } ) ;
276
+ if ( callback ) {
277
+ callback ( {
278
+ text : `Upload failed: ${ pathValidation . error } ` ,
279
+ content : { error : pathValidation . error }
280
+ } ) ;
281
+ }
282
+ return false ;
283
+ }
284
+
285
+ // Sanitize the file path
286
+ let sanitizedPath : string ;
287
+ try {
288
+ sanitizedPath = validator . sanitizePath ( filePath ) ;
289
+ elizaLogger . debug ( "File path sanitized" , {
290
+ originalPath : filePath ,
291
+ sanitizedPath,
292
+ messageId : message . id
293
+ } ) ;
294
+ } catch ( error ) {
295
+ const errorMessage = `Failed to sanitize file path: ${ error instanceof Error ? error . message : String ( error ) } ` ;
296
+ elizaLogger . error ( errorMessage , {
297
+ filePath,
298
+ messageId : message . id
299
+ } ) ;
300
+ if ( callback ) {
301
+ callback ( {
302
+ text : "Upload failed: Invalid file path." ,
303
+ content : { error : errorMessage }
304
+ } ) ;
305
+ }
135
306
return false ;
136
307
}
137
308
138
- await file . close ( ) ;
309
+ // Start upload monitoring
310
+ const startTime = Date . now ( ) ;
311
+ let fileStats ;
312
+ try {
313
+ fileStats = await fs . stat ( sanitizedPath ) ;
314
+ elizaLogger . debug ( "File stats retrieved" , {
315
+ size : fileStats . size ,
316
+ path : sanitizedPath ,
317
+ created : fileStats . birthtime ,
318
+ modified : fileStats . mtime ,
319
+ messageId : message . id
320
+ } ) ;
321
+ } catch ( error ) {
322
+ const errorMessage = `Failed to get file stats: ${ error instanceof Error ? error . message : String ( error ) } ` ;
323
+ elizaLogger . error ( errorMessage , {
324
+ path : sanitizedPath ,
325
+ messageId : message . id
326
+ } ) ;
327
+ if ( callback ) {
328
+ callback ( {
329
+ text : "Upload failed: Could not access file" ,
330
+ content : { error : errorMessage }
331
+ } ) ;
332
+ }
333
+ return false ;
334
+ }
335
+
336
+ try {
337
+ // Initialize ZeroG file
338
+ elizaLogger . debug ( "Initializing ZeroG file" , {
339
+ sanitizedPath,
340
+ messageId : message . id
341
+ } ) ;
342
+ file = await ZgFile . fromFilePath ( sanitizedPath ) ;
343
+ cleanupRequired = true ;
344
+
345
+ // Generate Merkle tree
346
+ elizaLogger . debug ( "Generating Merkle tree" ) ;
347
+ const [ merkleTree , merkleError ] = await file . merkleTree ( ) ;
348
+ if ( merkleError !== null ) {
349
+ const error = `Error getting file root hash: ${ merkleError instanceof Error ? merkleError . message : String ( merkleError ) } ` ;
350
+ elizaLogger . error ( error , { messageId : message . id } ) ;
351
+ if ( callback ) {
352
+ callback ( {
353
+ text : "Upload failed: Error generating file hash." ,
354
+ content : { error }
355
+ } ) ;
356
+ }
357
+ return false ;
358
+ }
359
+ elizaLogger . info ( "File root hash generated" , {
360
+ rootHash : merkleTree . rootHash ( ) ,
361
+ messageId : message . id
362
+ } ) ;
363
+
364
+ // Initialize blockchain connection
365
+ elizaLogger . debug ( "Initializing blockchain connection" ) ;
366
+ const provider = new ethers . JsonRpcProvider ( runtime . getSetting ( "ZEROG_EVM_RPC" ) ) ;
367
+ const signer = new ethers . Wallet ( runtime . getSetting ( "ZEROG_PRIVATE_KEY" ) , provider ) ;
368
+ const indexer = new Indexer ( runtime . getSetting ( "ZEROG_INDEXER_RPC" ) ) ;
369
+ const flowContract = getFlowContract ( runtime . getSetting ( "ZEROG_FLOW_ADDRESS" ) , signer ) ;
370
+
371
+ // Upload file to ZeroG
372
+ elizaLogger . info ( "Starting file upload to ZeroG" , {
373
+ filePath : sanitizedPath ,
374
+ messageId : message . id
375
+ } ) ;
376
+ const [ txHash , uploadError ] = await indexer . upload (
377
+ file ,
378
+ 0 ,
379
+ runtime . getSetting ( "ZEROG_EVM_RPC" ) ,
380
+ flowContract
381
+ ) ;
382
+
383
+ if ( uploadError !== null ) {
384
+ const error = `Error uploading file: ${ uploadError instanceof Error ? uploadError . message : String ( uploadError ) } ` ;
385
+ elizaLogger . error ( error , { messageId : message . id } ) ;
386
+ monitorUpload ( {
387
+ filePath : sanitizedPath ,
388
+ size : fileStats . size ,
389
+ duration : Date . now ( ) - startTime ,
390
+ success : false ,
391
+ error : error
392
+ } ) ;
393
+ if ( callback ) {
394
+ callback ( {
395
+ text : "Upload failed: Error during file upload." ,
396
+ content : { error }
397
+ } ) ;
398
+ }
399
+ return false ;
400
+ }
401
+
402
+ // Log successful upload
403
+ monitorUpload ( {
404
+ filePath : sanitizedPath ,
405
+ size : fileStats . size ,
406
+ duration : Date . now ( ) - startTime ,
407
+ success : true
408
+ } ) ;
409
+
410
+ elizaLogger . info ( "File uploaded successfully" , {
411
+ transactionHash : txHash ,
412
+ filePath : sanitizedPath ,
413
+ fileSize : fileStats . size ,
414
+ duration : Date . now ( ) - startTime ,
415
+ messageId : message . id
416
+ } ) ;
417
+
418
+ if ( callback ) {
419
+ callback ( {
420
+ text : "File uploaded successfully to ZeroG." ,
421
+ content : {
422
+ success : true ,
423
+ transactionHash : txHash
424
+ }
425
+ } ) ;
426
+ }
427
+
428
+ return true ;
429
+ } finally {
430
+ // Cleanup temporary file
431
+ if ( cleanupRequired && file ) {
432
+ try {
433
+ elizaLogger . debug ( "Starting file cleanup" , {
434
+ filePath : sanitizedPath ,
435
+ messageId : message . id
436
+ } ) ;
437
+ await file . close ( ) ;
438
+ await fs . unlink ( sanitizedPath ) ;
439
+ monitorCleanup ( sanitizedPath , true ) ;
440
+ elizaLogger . debug ( "File cleanup completed successfully" , {
441
+ filePath : sanitizedPath ,
442
+ messageId : message . id
443
+ } ) ;
444
+ } catch ( cleanupError ) {
445
+ monitorCleanup ( sanitizedPath , false , cleanupError . message ) ;
446
+ elizaLogger . warn ( "Failed to cleanup file" , {
447
+ error : cleanupError instanceof Error ? cleanupError . message : String ( cleanupError ) ,
448
+ filePath : sanitizedPath ,
449
+ messageId : message . id
450
+ } ) ;
451
+ }
452
+ }
453
+ }
139
454
} catch ( error ) {
140
- console . error ( "Error getting settings for 0G upload:" , error ) ;
455
+ const errorMessage = error instanceof Error ? error . message : String ( error ) ;
456
+ logSecurityEvent ( "Unexpected error in upload action" , "high" , {
457
+ error : errorMessage ,
458
+ stack : error instanceof Error ? error . stack : undefined ,
459
+ messageId : message . id
460
+ } ) ;
461
+
462
+ elizaLogger . error ( "Unexpected error during file upload" , {
463
+ error : errorMessage ,
464
+ stack : error instanceof Error ? error . stack : undefined ,
465
+ messageId : message . id
466
+ } ) ;
467
+
468
+ if ( callback ) {
469
+ callback ( {
470
+ text : "Upload failed due to an unexpected error." ,
471
+ content : { error : errorMessage }
472
+ } ) ;
473
+ }
474
+
475
+ return false ;
141
476
}
142
477
} ,
143
478
examples : [
0 commit comments