serverless-bedrock-agentcore-plugin 0.1.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/src/index.js ADDED
@@ -0,0 +1,1354 @@
1
+ 'use strict';
2
+
3
+ const { compileRuntime } = require('./compilers/runtime');
4
+ const { compileRuntimeEndpoint } = require('./compilers/runtimeEndpoint');
5
+ const { compileMemory } = require('./compilers/memory');
6
+ const { compileGateway } = require('./compilers/gateway');
7
+ const { compileGatewayTarget } = require('./compilers/gatewayTarget');
8
+ const { compileBrowser } = require('./compilers/browser');
9
+ const { compileCodeInterpreter } = require('./compilers/codeInterpreter');
10
+ const { compileWorkloadIdentity } = require('./compilers/workloadIdentity');
11
+ const {
12
+ generateRuntimeRole,
13
+ generateMemoryRole,
14
+ generateGatewayRole,
15
+ generateBrowserRole,
16
+ generateCodeInterpreterRole,
17
+ } = require('./iam/policies');
18
+ const { getLogicalId } = require('./utils/naming');
19
+ const { mergeTags } = require('./utils/tags');
20
+ const { defineAgentsSchema } = require('./validators/schema');
21
+ const { DockerBuilder } = require('./docker/builder');
22
+
23
+ /**
24
+ * Serverless Framework plugin for AWS Bedrock AgentCore
25
+ *
26
+ * Enables defining AgentCore Runtime, Memory, and Gateway resources
27
+ * directly in serverless.yml configuration files.
28
+ */
29
+ class ServerlessBedrockAgentCore {
30
+ constructor(serverless, options, { log, progress, writeText }) {
31
+ this.serverless = serverless;
32
+ this.options = options;
33
+ this.log = log;
34
+ this.progress = progress;
35
+ this.writeText = writeText;
36
+ this.provider = this.serverless.getProvider('aws');
37
+
38
+ // Plugin configuration
39
+ this.pluginName = 'serverless-bedrock-agentcore';
40
+
41
+ // Store built image URIs
42
+ this.builtImages = {};
43
+
44
+ // Flag to prevent compiling resources multiple times
45
+ this.resourcesCompiled = false;
46
+
47
+ // Define schema validation for 'agents' top-level key
48
+ defineAgentsSchema(serverless);
49
+
50
+ // Define custom commands
51
+ this.commands = {
52
+ agentcore: {
53
+ usage: 'Manage Bedrock AgentCore resources',
54
+ lifecycleEvents: ['info'],
55
+ commands: {
56
+ info: {
57
+ usage: 'Display information about deployed AgentCore resources',
58
+ lifecycleEvents: ['info'],
59
+ options: {
60
+ verbose: {
61
+ usage: 'Show detailed information',
62
+ shortcut: 'v',
63
+ type: 'boolean',
64
+ },
65
+ },
66
+ },
67
+ build: {
68
+ usage: 'Build Docker images for AgentCore runtimes',
69
+ lifecycleEvents: ['build'],
70
+ },
71
+ invoke: {
72
+ usage: 'Invoke a deployed AgentCore runtime agent',
73
+ lifecycleEvents: ['invoke'],
74
+ options: {
75
+ agent: {
76
+ usage: 'Name of the agent to invoke (defaults to first runtime agent)',
77
+ shortcut: 'a',
78
+ type: 'string',
79
+ },
80
+ message: {
81
+ usage: 'Message to send to the agent',
82
+ shortcut: 'm',
83
+ type: 'string',
84
+ required: true,
85
+ },
86
+ session: {
87
+ usage: 'Session ID for conversation continuity',
88
+ shortcut: 's',
89
+ type: 'string',
90
+ },
91
+ },
92
+ },
93
+ logs: {
94
+ usage: 'Fetch logs for a deployed AgentCore runtime',
95
+ lifecycleEvents: ['logs'],
96
+ options: {
97
+ agent: {
98
+ usage: 'Name of the agent to get logs for (defaults to first runtime agent)',
99
+ shortcut: 'a',
100
+ type: 'string',
101
+ },
102
+ tail: {
103
+ usage: 'Continuously stream new logs',
104
+ shortcut: 't',
105
+ type: 'boolean',
106
+ },
107
+ startTime: {
108
+ usage: 'Start time for logs (e.g., "1h", "30m", "2024-01-01")',
109
+ type: 'string',
110
+ },
111
+ filter: {
112
+ usage: 'Filter pattern for logs',
113
+ shortcut: 'f',
114
+ type: 'string',
115
+ },
116
+ },
117
+ },
118
+ },
119
+ },
120
+ };
121
+
122
+ // Lifecycle hooks
123
+ this.hooks = {
124
+ // Initialization
125
+ initialize: () => this.init(),
126
+
127
+ // Validation phase
128
+ 'before:package:initialize': () => this.validateConfig(),
129
+
130
+ // Build Docker images before packaging (if configured)
131
+ 'before:package:createDeploymentArtifacts': () => this.buildDockerImages(),
132
+
133
+ // Compilation phase - add resources to CloudFormation
134
+ // Try multiple hooks to ensure compatibility with different Serverless versions
135
+ 'package:compileEvents': () => this.compileAgentCoreResources(),
136
+ 'before:package:compileFunctions': () => this.compileAgentCoreResources(),
137
+ 'after:package:compileFunctions': () => this.compileAgentCoreResources(),
138
+ 'before:package:finalize': () => this.compileAgentCoreResources(),
139
+
140
+ // Post-deploy info
141
+ 'after:deploy:deploy': () => this.displayDeploymentInfo(),
142
+
143
+ // Custom commands
144
+ 'agentcore:info:info': () => this.showInfo(),
145
+ 'agentcore:build:build': () => this.buildDockerImages(),
146
+ 'agentcore:invoke:invoke': () => this.invokeAgent(),
147
+ 'agentcore:logs:logs': () => this.fetchLogs(),
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Initialize plugin
153
+ */
154
+ init() {
155
+ this.log.debug(`${this.pluginName} initialized`);
156
+ }
157
+
158
+ /**
159
+ * Get the service context for compilers
160
+ */
161
+ getContext() {
162
+ const service = this.serverless.service;
163
+ const stage = this.provider.getStage();
164
+ const region = this.provider.getRegion();
165
+ const customConfig = service.custom?.agentCore || {};
166
+
167
+ return {
168
+ serviceName: service.service,
169
+ stage,
170
+ region,
171
+ accountId: '${AWS::AccountId}', // CloudFormation intrinsic
172
+ customConfig,
173
+ defaultTags: customConfig.defaultTags || {},
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Validate the agents configuration
179
+ */
180
+ validateConfig() {
181
+ const agents = this.getAgentsConfig();
182
+
183
+ if (!agents || Object.keys(agents).length === 0) {
184
+ this.log.debug('No agents defined, skipping AgentCore compilation');
185
+
186
+ return;
187
+ }
188
+
189
+ this.log.info(`Validating ${Object.keys(agents).length} agent(s)...`);
190
+
191
+ for (const [name, config] of Object.entries(agents)) {
192
+ this.validateAgent(name, config);
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Validate individual agent configuration
198
+ */
199
+ validateAgent(name, config) {
200
+ if (!config.type) {
201
+ throw new this.serverless.classes.Error(
202
+ `Agent '${name}' must have a 'type' property (runtime, memory, gateway, browser, codeInterpreter, or workloadIdentity)`
203
+ );
204
+ }
205
+
206
+ const validTypes = [
207
+ 'runtime',
208
+ 'memory',
209
+ 'gateway',
210
+ 'browser',
211
+ 'codeInterpreter',
212
+ 'workloadIdentity',
213
+ ];
214
+ if (!validTypes.includes(config.type)) {
215
+ throw new this.serverless.classes.Error(
216
+ `Agent '${name}' has invalid type '${config.type}'. Valid types: ${validTypes.join(', ')}`
217
+ );
218
+ }
219
+
220
+ // Type-specific validation
221
+ switch (config.type) {
222
+ case 'runtime':
223
+ this.validateRuntime(name, config);
224
+ break;
225
+ case 'memory':
226
+ this.validateMemory(name, config);
227
+ break;
228
+ case 'gateway':
229
+ this.validateGateway(name, config);
230
+ break;
231
+ case 'browser':
232
+ this.validateBrowser(name, config);
233
+ break;
234
+ case 'codeInterpreter':
235
+ this.validateCodeInterpreter(name, config);
236
+ break;
237
+ case 'workloadIdentity':
238
+ this.validateWorkloadIdentity(name, config);
239
+ break;
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Validate runtime configuration
245
+ */
246
+ validateRuntime(name, config) {
247
+ // Check if using image reference (will be built) or artifact (pre-built)
248
+ const hasImage = config.image !== undefined;
249
+ const hasArtifact = config.artifact !== undefined;
250
+
251
+ if (!hasImage && !hasArtifact) {
252
+ throw new this.serverless.classes.Error(
253
+ `Runtime '${name}' must have either 'image' (for Docker build) or 'artifact' (for pre-built image)`
254
+ );
255
+ }
256
+
257
+ if (hasArtifact) {
258
+ // artifact.docker means we need to build
259
+ if (config.artifact.docker) {
260
+ // Docker build config is valid
261
+ } else if (!config.artifact.containerImage && !config.artifact.s3) {
262
+ // Otherwise need containerImage or s3
263
+ throw new this.serverless.classes.Error(
264
+ `Runtime '${name}' artifact must specify either 'containerImage', 's3' (bucket/key), or 'docker' (for build)`
265
+ );
266
+ }
267
+ }
268
+
269
+ // Validate requestHeaders configuration
270
+ if (config.requestHeaders) {
271
+ if (config.requestHeaders.allowlist) {
272
+ if (!Array.isArray(config.requestHeaders.allowlist)) {
273
+ throw new this.serverless.classes.Error(
274
+ `Runtime '${name}' requestHeaders.allowlist must be an array of header names`
275
+ );
276
+ }
277
+ if (config.requestHeaders.allowlist.length > 20) {
278
+ throw new this.serverless.classes.Error(
279
+ `Runtime '${name}' requestHeaders.allowlist cannot exceed 20 headers (got ${config.requestHeaders.allowlist.length})`
280
+ );
281
+ }
282
+ // Validate each header is a non-empty string
283
+ for (const header of config.requestHeaders.allowlist) {
284
+ if (typeof header !== 'string' || header.trim().length === 0) {
285
+ throw new this.serverless.classes.Error(
286
+ `Runtime '${name}' requestHeaders.allowlist contains invalid header name: '${header}'`
287
+ );
288
+ }
289
+ }
290
+ }
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Validate memory configuration
296
+ */
297
+ validateMemory(name, config) {
298
+ if (config.eventExpiryDuration !== undefined) {
299
+ const duration = config.eventExpiryDuration;
300
+ if (typeof duration !== 'number' || duration < 7 || duration > 365) {
301
+ throw new this.serverless.classes.Error(
302
+ `Memory '${name}' eventExpiryDuration must be a number between 7 and 365 days`
303
+ );
304
+ }
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Validate gateway configuration
310
+ */
311
+ validateGateway(name, config) {
312
+ const validAuthTypes = ['AWS_IAM', 'CUSTOM_JWT'];
313
+ if (config.authorizerType && !validAuthTypes.includes(config.authorizerType)) {
314
+ throw new this.serverless.classes.Error(
315
+ `Gateway '${name}' has invalid authorizerType '${config.authorizerType}'. Valid types: ${validAuthTypes.join(', ')}`
316
+ );
317
+ }
318
+
319
+ const validProtocols = ['MCP'];
320
+ if (config.protocolType && !validProtocols.includes(config.protocolType)) {
321
+ throw new this.serverless.classes.Error(
322
+ `Gateway '${name}' has invalid protocolType '${config.protocolType}'. Valid types: ${validProtocols.join(', ')}`
323
+ );
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Validate browser configuration
329
+ */
330
+ validateBrowser(name, config) {
331
+ const validModes = ['PUBLIC', 'VPC'];
332
+ if (config.network?.networkMode && !validModes.includes(config.network.networkMode)) {
333
+ throw new this.serverless.classes.Error(
334
+ `Browser '${name}' has invalid networkMode '${config.network.networkMode}'. Valid modes: ${validModes.join(', ')}`
335
+ );
336
+ }
337
+
338
+ // Validate recording configuration
339
+ if (config.recording) {
340
+ if (config.recording.s3Location) {
341
+ if (!config.recording.s3Location.bucket) {
342
+ throw new this.serverless.classes.Error(
343
+ `Browser '${name}' recording.s3Location must have a 'bucket' property`
344
+ );
345
+ }
346
+ }
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Validate code interpreter configuration
352
+ */
353
+ validateCodeInterpreter(name, config) {
354
+ const validModes = ['PUBLIC', 'SANDBOX', 'VPC'];
355
+ if (config.network?.networkMode && !validModes.includes(config.network.networkMode)) {
356
+ throw new this.serverless.classes.Error(
357
+ `CodeInterpreter '${name}' has invalid networkMode '${config.network.networkMode}'. Valid modes: ${validModes.join(', ')}`
358
+ );
359
+ }
360
+
361
+ // Validate VPC configuration when VPC mode is specified
362
+ if (config.network?.networkMode === 'VPC') {
363
+ if (!config.network.vpcConfig) {
364
+ throw new this.serverless.classes.Error(
365
+ `CodeInterpreter '${name}' requires vpcConfig when networkMode is VPC`
366
+ );
367
+ }
368
+ if (!config.network.vpcConfig.subnetIds || config.network.vpcConfig.subnetIds.length === 0) {
369
+ throw new this.serverless.classes.Error(
370
+ `CodeInterpreter '${name}' vpcConfig must have at least one subnetId`
371
+ );
372
+ }
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Validate workload identity configuration
378
+ */
379
+ validateWorkloadIdentity(name, config) {
380
+ // Name must be 3-255 characters, pattern: [A-Za-z0-9_.-]+
381
+ // The naming utility will handle this, but we can validate length
382
+ if (name.length < 1 || name.length > 255) {
383
+ throw new this.serverless.classes.Error(
384
+ `WorkloadIdentity '${name}' name must be between 1 and 255 characters`
385
+ );
386
+ }
387
+
388
+ // Validate OAuth2 return URLs if provided
389
+ if (config.oauth2ReturnUrls) {
390
+ if (!Array.isArray(config.oauth2ReturnUrls)) {
391
+ throw new this.serverless.classes.Error(
392
+ `WorkloadIdentity '${name}' oauth2ReturnUrls must be an array`
393
+ );
394
+ }
395
+
396
+ for (const url of config.oauth2ReturnUrls) {
397
+ // Allow https:// URLs or http://localhost for local development
398
+ const isHttps = url.startsWith('https://');
399
+ const isLocalhost = url.startsWith('http://localhost');
400
+ if (typeof url !== 'string' || (!isHttps && !isLocalhost)) {
401
+ throw new this.serverless.classes.Error(
402
+ `WorkloadIdentity '${name}' oauth2ReturnUrls must contain valid HTTPS URLs (or http://localhost for development)`
403
+ );
404
+ }
405
+ }
406
+ }
407
+ }
408
+
409
+ /**
410
+ * Build Docker images for runtime agents that have image configuration
411
+ */
412
+ async buildDockerImages() {
413
+ const agents = this.getAgentsConfig();
414
+ const ecrImages = this.serverless.service.provider?.ecr?.images;
415
+
416
+ if (!agents) {
417
+ return;
418
+ }
419
+
420
+ // Find runtimes that need Docker builds
421
+ const runtimesToBuild = [];
422
+
423
+ for (const [name, config] of Object.entries(agents)) {
424
+ if (config.type === 'runtime') {
425
+ // Check for image config (legacy) or artifact.docker (new)
426
+ if (config.image) {
427
+ runtimesToBuild.push({ name, config, imageConfig: config.image });
428
+ } else if (config.artifact?.docker) {
429
+ runtimesToBuild.push({ name, config, imageConfig: config.artifact.docker });
430
+ }
431
+ }
432
+ }
433
+
434
+ if (runtimesToBuild.length === 0 && !ecrImages) {
435
+ return;
436
+ }
437
+
438
+ // Initialize Docker builder
439
+ const builder = new DockerBuilder(this.serverless, this.log, this.progress);
440
+
441
+ // Check Docker is available
442
+ if (!builder.checkDocker()) {
443
+ throw new this.serverless.classes.Error(
444
+ 'Docker is required to build agent images but was not found. Please install Docker.'
445
+ );
446
+ }
447
+
448
+ const context = this.getContext();
449
+
450
+ // Build images defined in provider.ecr.images (Serverless standard pattern)
451
+ if (ecrImages) {
452
+ this.log.info('Building ECR images...');
453
+ this.builtImages = await builder.processImages(ecrImages, context);
454
+ }
455
+
456
+ // Build images for runtimes with docker config
457
+ for (const { name, imageConfig } of runtimesToBuild) {
458
+ // Handle string reference to provider.ecr.images
459
+ if (typeof imageConfig === 'string') {
460
+ if (this.builtImages[imageConfig]) {
461
+ continue; // Already built above
462
+ }
463
+ }
464
+
465
+ // Build the image - imageConfig should have path or repository
466
+ const dockerConfig = typeof imageConfig === 'string' ? { name: imageConfig } : imageConfig;
467
+
468
+ if (dockerConfig.path || dockerConfig.repository) {
469
+ this.log.info(`Building Docker image for runtime: ${name}`);
470
+ const imageUri = await builder.buildAndPushForRuntime(name, dockerConfig, context);
471
+ this.builtImages[name] = imageUri;
472
+ }
473
+ }
474
+ }
475
+
476
+ /**
477
+ * Get agents configuration from various sources
478
+ */
479
+ getAgentsConfig() {
480
+ // Try multiple locations where agents might be defined
481
+ const service = this.serverless.service;
482
+
483
+ // Direct property on service
484
+ if (service.agents) {
485
+ return service.agents;
486
+ }
487
+
488
+ // From initialServerlessConfig (raw yaml)
489
+ if (service.initialServerlessConfig?.agents) {
490
+ return service.initialServerlessConfig.agents;
491
+ }
492
+
493
+ // From custom.agents (alternative location)
494
+ if (service.custom?.agents) {
495
+ return service.custom.agents;
496
+ }
497
+
498
+ // From serverless.configurationInput
499
+ if (this.serverless.configurationInput?.agents) {
500
+ return this.serverless.configurationInput.agents;
501
+ }
502
+
503
+ return null;
504
+ }
505
+
506
+ /**
507
+ * Compile all AgentCore resources to CloudFormation
508
+ */
509
+ compileAgentCoreResources() {
510
+ // Prevent running multiple times
511
+ if (this.resourcesCompiled) {
512
+ return;
513
+ }
514
+
515
+ const agents = this.getAgentsConfig();
516
+
517
+ if (!agents || Object.keys(agents).length === 0) {
518
+ return;
519
+ }
520
+
521
+ this.resourcesCompiled = true;
522
+
523
+ const context = this.getContext();
524
+ const template = this.serverless.service.provider.compiledCloudFormationTemplate;
525
+
526
+ if (!template) {
527
+ this.resourcesCompiled = false;
528
+
529
+ return;
530
+ }
531
+
532
+ // Ensure Resources and Outputs exist
533
+ template.Resources = template.Resources || {};
534
+ template.Outputs = template.Outputs || {};
535
+
536
+ const stats = {
537
+ runtime: 0,
538
+ memory: 0,
539
+ gateway: 0,
540
+ browser: 0,
541
+ codeInterpreter: 0,
542
+ workloadIdentity: 0,
543
+ endpoints: 0,
544
+ targets: 0,
545
+ };
546
+
547
+ for (const [name, config] of Object.entries(agents)) {
548
+ switch (config.type) {
549
+ case 'runtime':
550
+ this.compileRuntimeResources(name, config, context, template);
551
+ stats.runtime++;
552
+ if (config.endpoints) {
553
+ stats.endpoints += config.endpoints.length;
554
+ }
555
+ break;
556
+ case 'memory':
557
+ this.compileMemoryResources(name, config, context, template);
558
+ stats.memory++;
559
+ break;
560
+ case 'gateway':
561
+ this.compileGatewayResources(name, config, context, template);
562
+ stats.gateway++;
563
+ if (config.targets) {
564
+ stats.targets += config.targets.length;
565
+ }
566
+ break;
567
+ case 'browser':
568
+ this.compileBrowserResources(name, config, context, template);
569
+ stats.browser++;
570
+ break;
571
+ case 'codeInterpreter':
572
+ this.compileCodeInterpreterResources(name, config, context, template);
573
+ stats.codeInterpreter++;
574
+ break;
575
+ case 'workloadIdentity':
576
+ this.compileWorkloadIdentityResources(name, config, context, template);
577
+ stats.workloadIdentity++;
578
+ break;
579
+ }
580
+ }
581
+
582
+ const resourceSummary = [];
583
+ if (stats.runtime > 0) {
584
+ resourceSummary.push(`${stats.runtime} runtime(s)`);
585
+ }
586
+ if (stats.memory > 0) {
587
+ resourceSummary.push(`${stats.memory} memory(s)`);
588
+ }
589
+ if (stats.gateway > 0) {
590
+ resourceSummary.push(`${stats.gateway} gateway(s)`);
591
+ }
592
+ if (stats.browser > 0) {
593
+ resourceSummary.push(`${stats.browser} browser(s)`);
594
+ }
595
+ if (stats.codeInterpreter > 0) {
596
+ resourceSummary.push(`${stats.codeInterpreter} codeInterpreter(s)`);
597
+ }
598
+ if (stats.workloadIdentity > 0) {
599
+ resourceSummary.push(`${stats.workloadIdentity} workloadIdentity(s)`);
600
+ }
601
+
602
+ this.log.info(`Compiled AgentCore resources: ${resourceSummary.join(', ')}`);
603
+
604
+ if (stats.endpoints > 0) {
605
+ this.log.info(` - ${stats.endpoints} runtime endpoint(s)`);
606
+ }
607
+ if (stats.targets > 0) {
608
+ this.log.info(` - ${stats.targets} gateway target(s)`);
609
+ }
610
+ }
611
+
612
+ /**
613
+ * Resolve the container image for a runtime
614
+ */
615
+ resolveContainerImage(name, config) {
616
+ // If artifact.containerImage is specified, use it directly
617
+ if (config.artifact?.containerImage) {
618
+ return config.artifact.containerImage;
619
+ }
620
+
621
+ // If artifact.docker is specified, check if we built it
622
+ if (config.artifact?.docker) {
623
+ if (this.builtImages[name]) {
624
+ return this.builtImages[name];
625
+ }
626
+ }
627
+
628
+ // If image is specified, resolve it
629
+ if (config.image) {
630
+ const imageName = typeof config.image === 'string' ? config.image : name;
631
+
632
+ // Check if we built this image
633
+ if (this.builtImages[imageName]) {
634
+ return this.builtImages[imageName];
635
+ }
636
+
637
+ // Check provider.ecr.images for URI
638
+ const ecrImages = this.serverless.service.provider?.ecr?.images;
639
+ if (ecrImages && ecrImages[imageName]?.uri) {
640
+ return ecrImages[imageName].uri;
641
+ }
642
+ }
643
+
644
+ return null;
645
+ }
646
+
647
+ /**
648
+ * Compile Runtime and RuntimeEndpoint resources
649
+ */
650
+ compileRuntimeResources(name, config, context, template) {
651
+ const logicalId = getLogicalId(name, 'Runtime');
652
+ const tags = mergeTags(
653
+ context.defaultTags,
654
+ config.tags,
655
+ context.serviceName,
656
+ context.stage,
657
+ name
658
+ );
659
+
660
+ // Resolve container image
661
+ const containerImage = this.resolveContainerImage(name, config);
662
+
663
+ // Create a modified config with resolved image
664
+ const resolvedConfig = {
665
+ ...config,
666
+ artifact: containerImage ? { containerImage } : config.artifact,
667
+ };
668
+
669
+ // Generate IAM role if not provided
670
+ if (!config.roleArn) {
671
+ const roleLogicalId = `${logicalId}Role`;
672
+ const roleResource = generateRuntimeRole(name, resolvedConfig, context);
673
+ template.Resources[roleLogicalId] = roleResource;
674
+
675
+ // Output the role ARN
676
+ template.Outputs[`${logicalId}RoleArn`] = {
677
+ Description: `IAM Role ARN for ${name} runtime`,
678
+ Value: { 'Fn::GetAtt': [roleLogicalId, 'Arn'] },
679
+ };
680
+ }
681
+
682
+ // Compile Runtime resource
683
+ const runtimeResource = compileRuntime(name, resolvedConfig, context, tags);
684
+ template.Resources[logicalId] = runtimeResource;
685
+
686
+ // Add outputs
687
+ template.Outputs[`${logicalId}Arn`] = {
688
+ Description: `ARN of ${name} AgentCore Runtime`,
689
+ Value: { 'Fn::GetAtt': [logicalId, 'AgentRuntimeArn'] },
690
+ Export: {
691
+ Name: `${context.serviceName}-${context.stage}-${name}-RuntimeArn`,
692
+ },
693
+ };
694
+
695
+ template.Outputs[`${logicalId}Id`] = {
696
+ Description: `ID of ${name} AgentCore Runtime`,
697
+ Value: { 'Fn::GetAtt': [logicalId, 'AgentRuntimeId'] },
698
+ };
699
+
700
+ // Compile RuntimeEndpoints if defined
701
+ if (config.endpoints && config.endpoints.length > 0) {
702
+ for (const endpoint of config.endpoints) {
703
+ const endpointName = endpoint.name || 'default';
704
+ const endpointLogicalId = getLogicalId(name, `${endpointName}Endpoint`);
705
+
706
+ const endpointResource = compileRuntimeEndpoint(
707
+ name,
708
+ endpointName,
709
+ endpoint,
710
+ logicalId,
711
+ context,
712
+ tags
713
+ );
714
+ template.Resources[endpointLogicalId] = endpointResource;
715
+
716
+ // Add endpoint outputs
717
+ template.Outputs[`${endpointLogicalId}Arn`] = {
718
+ Description: `ARN of ${name}/${endpointName} RuntimeEndpoint`,
719
+ Value: { 'Fn::GetAtt': [endpointLogicalId, 'AgentRuntimeEndpointArn'] },
720
+ };
721
+ }
722
+ }
723
+ }
724
+
725
+ /**
726
+ * Compile Memory resources
727
+ */
728
+ compileMemoryResources(name, config, context, template) {
729
+ const logicalId = getLogicalId(name, 'Memory');
730
+ const tags = mergeTags(
731
+ context.defaultTags,
732
+ config.tags,
733
+ context.serviceName,
734
+ context.stage,
735
+ name
736
+ );
737
+
738
+ // Generate IAM role if not provided
739
+ if (!config.roleArn) {
740
+ const roleLogicalId = `${logicalId}Role`;
741
+ const roleResource = generateMemoryRole(name, config, context);
742
+ template.Resources[roleLogicalId] = roleResource;
743
+
744
+ template.Outputs[`${logicalId}RoleArn`] = {
745
+ Description: `IAM Role ARN for ${name} memory`,
746
+ Value: { 'Fn::GetAtt': [roleLogicalId, 'Arn'] },
747
+ };
748
+ }
749
+
750
+ // Compile Memory resource
751
+ const memoryResource = compileMemory(name, config, context, tags);
752
+ template.Resources[logicalId] = memoryResource;
753
+
754
+ // Add outputs
755
+ template.Outputs[`${logicalId}Arn`] = {
756
+ Description: `ARN of ${name} AgentCore Memory`,
757
+ Value: { 'Fn::GetAtt': [logicalId, 'MemoryArn'] },
758
+ Export: {
759
+ Name: `${context.serviceName}-${context.stage}-${name}-MemoryArn`,
760
+ },
761
+ };
762
+
763
+ template.Outputs[`${logicalId}Id`] = {
764
+ Description: `ID of ${name} AgentCore Memory`,
765
+ Value: { 'Fn::GetAtt': [logicalId, 'MemoryId'] },
766
+ };
767
+ }
768
+
769
+ /**
770
+ * Compile Gateway and GatewayTarget resources
771
+ */
772
+ compileGatewayResources(name, config, context, template) {
773
+ const logicalId = getLogicalId(name, 'Gateway');
774
+ const tags = mergeTags(
775
+ context.defaultTags,
776
+ config.tags,
777
+ context.serviceName,
778
+ context.stage,
779
+ name
780
+ );
781
+
782
+ // Generate IAM role if not provided
783
+ if (!config.roleArn) {
784
+ const roleLogicalId = `${logicalId}Role`;
785
+ const roleResource = generateGatewayRole(name, config, context);
786
+ template.Resources[roleLogicalId] = roleResource;
787
+
788
+ template.Outputs[`${logicalId}RoleArn`] = {
789
+ Description: `IAM Role ARN for ${name} gateway`,
790
+ Value: { 'Fn::GetAtt': [roleLogicalId, 'Arn'] },
791
+ };
792
+ }
793
+
794
+ // Compile Gateway resource
795
+ const gatewayResource = compileGateway(name, config, context, tags);
796
+ template.Resources[logicalId] = gatewayResource;
797
+
798
+ // Add outputs
799
+ template.Outputs[`${logicalId}Arn`] = {
800
+ Description: `ARN of ${name} AgentCore Gateway`,
801
+ Value: { 'Fn::GetAtt': [logicalId, 'GatewayArn'] },
802
+ Export: {
803
+ Name: `${context.serviceName}-${context.stage}-${name}-GatewayArn`,
804
+ },
805
+ };
806
+
807
+ template.Outputs[`${logicalId}Id`] = {
808
+ Description: `ID of ${name} AgentCore Gateway`,
809
+ Value: { 'Fn::GetAtt': [logicalId, 'GatewayIdentifier'] },
810
+ };
811
+
812
+ template.Outputs[`${logicalId}Url`] = {
813
+ Description: `URL of ${name} AgentCore Gateway`,
814
+ Value: { 'Fn::GetAtt': [logicalId, 'GatewayUrl'] },
815
+ };
816
+
817
+ // Compile GatewayTargets if defined
818
+ if (config.targets && config.targets.length > 0) {
819
+ for (const target of config.targets) {
820
+ const targetName = target.name;
821
+ if (!targetName) {
822
+ throw new this.serverless.classes.Error(
823
+ `Gateway '${name}' target must have a 'name' property`
824
+ );
825
+ }
826
+
827
+ const targetLogicalId = getLogicalId(name, `${targetName}Target`);
828
+ const targetResource = compileGatewayTarget(name, targetName, target, logicalId, context);
829
+ template.Resources[targetLogicalId] = targetResource;
830
+
831
+ // Add target outputs
832
+ template.Outputs[`${targetLogicalId}Id`] = {
833
+ Description: `ID of ${name}/${targetName} GatewayTarget`,
834
+ Value: { 'Fn::GetAtt': [targetLogicalId, 'TargetId'] },
835
+ };
836
+ }
837
+ }
838
+ }
839
+
840
+ /**
841
+ * Compile Browser resources
842
+ */
843
+ compileBrowserResources(name, config, context, template) {
844
+ const logicalId = getLogicalId(name, 'Browser');
845
+ const tags = mergeTags(
846
+ context.defaultTags,
847
+ config.tags,
848
+ context.serviceName,
849
+ context.stage,
850
+ name
851
+ );
852
+
853
+ // Generate IAM role if not provided
854
+ if (!config.roleArn) {
855
+ const roleLogicalId = `${logicalId}Role`;
856
+ const roleResource = generateBrowserRole(name, config, context);
857
+ template.Resources[roleLogicalId] = roleResource;
858
+
859
+ template.Outputs[`${logicalId}RoleArn`] = {
860
+ Description: `IAM Role ARN for ${name} browser`,
861
+ Value: { 'Fn::GetAtt': [roleLogicalId, 'Arn'] },
862
+ };
863
+ }
864
+
865
+ // Compile Browser resource
866
+ const browserResource = compileBrowser(name, config, context, tags);
867
+ template.Resources[logicalId] = browserResource;
868
+
869
+ // Add outputs
870
+ template.Outputs[`${logicalId}Arn`] = {
871
+ Description: `ARN of ${name} AgentCore Browser`,
872
+ Value: { 'Fn::GetAtt': [logicalId, 'BrowserArn'] },
873
+ Export: {
874
+ Name: `${context.serviceName}-${context.stage}-${name}-BrowserArn`,
875
+ },
876
+ };
877
+
878
+ template.Outputs[`${logicalId}Id`] = {
879
+ Description: `ID of ${name} AgentCore Browser`,
880
+ Value: { 'Fn::GetAtt': [logicalId, 'BrowserId'] },
881
+ };
882
+ }
883
+
884
+ /**
885
+ * Compile CodeInterpreter resources
886
+ */
887
+ compileCodeInterpreterResources(name, config, context, template) {
888
+ const logicalId = getLogicalId(name, 'CodeInterpreter');
889
+ const tags = mergeTags(
890
+ context.defaultTags,
891
+ config.tags,
892
+ context.serviceName,
893
+ context.stage,
894
+ name
895
+ );
896
+
897
+ // Generate IAM role if not provided
898
+ if (!config.roleArn) {
899
+ const roleLogicalId = `${logicalId}Role`;
900
+ const roleResource = generateCodeInterpreterRole(name, config, context);
901
+ template.Resources[roleLogicalId] = roleResource;
902
+
903
+ template.Outputs[`${logicalId}RoleArn`] = {
904
+ Description: `IAM Role ARN for ${name} code interpreter`,
905
+ Value: { 'Fn::GetAtt': [roleLogicalId, 'Arn'] },
906
+ };
907
+ }
908
+
909
+ // Compile CodeInterpreter resource
910
+ const codeInterpreterResource = compileCodeInterpreter(name, config, context, tags);
911
+ template.Resources[logicalId] = codeInterpreterResource;
912
+
913
+ // Add outputs
914
+ template.Outputs[`${logicalId}Arn`] = {
915
+ Description: `ARN of ${name} AgentCore CodeInterpreter`,
916
+ Value: { 'Fn::GetAtt': [logicalId, 'CodeInterpreterArn'] },
917
+ Export: {
918
+ Name: `${context.serviceName}-${context.stage}-${name}-CodeInterpreterArn`,
919
+ },
920
+ };
921
+
922
+ template.Outputs[`${logicalId}Id`] = {
923
+ Description: `ID of ${name} AgentCore CodeInterpreter`,
924
+ Value: { 'Fn::GetAtt': [logicalId, 'CodeInterpreterId'] },
925
+ };
926
+ }
927
+
928
+ /**
929
+ * Compile WorkloadIdentity resources
930
+ */
931
+ compileWorkloadIdentityResources(name, config, context, template) {
932
+ const logicalId = getLogicalId(name, 'WorkloadIdentity');
933
+ const tags = mergeTags(
934
+ context.defaultTags,
935
+ config.tags,
936
+ context.serviceName,
937
+ context.stage,
938
+ name
939
+ );
940
+
941
+ // WorkloadIdentity doesn't require an IAM role
942
+
943
+ // Compile WorkloadIdentity resource
944
+ const workloadIdentityResource = compileWorkloadIdentity(name, config, context, tags);
945
+ template.Resources[logicalId] = workloadIdentityResource;
946
+
947
+ // Add outputs
948
+ template.Outputs[`${logicalId}Arn`] = {
949
+ Description: `ARN of ${name} AgentCore WorkloadIdentity`,
950
+ Value: { 'Fn::GetAtt': [logicalId, 'WorkloadIdentityArn'] },
951
+ Export: {
952
+ Name: `${context.serviceName}-${context.stage}-${name}-WorkloadIdentityArn`,
953
+ },
954
+ };
955
+ }
956
+
957
+ /**
958
+ * Display deployment information after deploy
959
+ */
960
+ async displayDeploymentInfo() {
961
+ const agents = this.serverless.service.agents;
962
+
963
+ if (!agents || Object.keys(agents).length === 0) {
964
+ return;
965
+ }
966
+
967
+ this.log.notice('AgentCore Resources Deployed:');
968
+
969
+ for (const [name, config] of Object.entries(agents)) {
970
+ const type = config.type.charAt(0).toUpperCase() + config.type.slice(1);
971
+ this.log.notice(` ${name} (${type})`);
972
+ }
973
+ }
974
+
975
+ /**
976
+ * Show information about AgentCore resources
977
+ */
978
+ async showInfo() {
979
+ const agents = this.serverless.service.agents;
980
+
981
+ if (!agents || Object.keys(agents).length === 0) {
982
+ this.log.notice('No AgentCore resources defined in this service.');
983
+
984
+ return;
985
+ }
986
+
987
+ this.log.notice('AgentCore Resources:');
988
+ this.log.notice('');
989
+
990
+ for (const [name, config] of Object.entries(agents)) {
991
+ const type = config.type.charAt(0).toUpperCase() + config.type.slice(1);
992
+ this.log.notice(` ${name}:`);
993
+ this.log.notice(` Type: ${type}`);
994
+
995
+ if (config.description) {
996
+ this.log.notice(` Description: ${config.description}`);
997
+ }
998
+
999
+ if (this.options.verbose) {
1000
+ this.log.notice(` Config: ${JSON.stringify(config, null, 2)}`);
1001
+ }
1002
+
1003
+ this.log.notice('');
1004
+ }
1005
+ }
1006
+
1007
+ /**
1008
+ * Get the runtime ARN for an agent from CloudFormation stack outputs
1009
+ */
1010
+ async getRuntimeArn(agentName) {
1011
+ const stackName = this.provider.naming.getStackName();
1012
+
1013
+ try {
1014
+ const result = await this.provider.request('CloudFormation', 'describeStacks', {
1015
+ StackName: stackName,
1016
+ });
1017
+
1018
+ const stack = result.Stacks?.[0];
1019
+ if (!stack) {
1020
+ throw new Error(`Stack ${stackName} not found`);
1021
+ }
1022
+
1023
+ // Look for the runtime ARN output
1024
+ const logicalId = getLogicalId(agentName, 'Runtime');
1025
+ const outputKey = `${logicalId}Arn`;
1026
+
1027
+ const output = stack.Outputs?.find((o) => o.OutputKey === outputKey);
1028
+ if (!output) {
1029
+ throw new Error(`Runtime ARN output not found for agent '${agentName}'`);
1030
+ }
1031
+
1032
+ return output.OutputValue;
1033
+ } catch (error) {
1034
+ throw new this.serverless.classes.Error(`Failed to get runtime ARN: ${error.message}`);
1035
+ }
1036
+ }
1037
+
1038
+ /**
1039
+ * Get the runtime ID for an agent from CloudFormation stack outputs
1040
+ */
1041
+ async getRuntimeId(agentName) {
1042
+ const stackName = this.provider.naming.getStackName();
1043
+
1044
+ try {
1045
+ const result = await this.provider.request('CloudFormation', 'describeStacks', {
1046
+ StackName: stackName,
1047
+ });
1048
+
1049
+ const stack = result.Stacks?.[0];
1050
+ if (!stack) {
1051
+ throw new Error(`Stack ${stackName} not found`);
1052
+ }
1053
+
1054
+ const logicalId = getLogicalId(agentName, 'Runtime');
1055
+ const outputKey = `${logicalId}Id`;
1056
+
1057
+ const output = stack.Outputs?.find((o) => o.OutputKey === outputKey);
1058
+ if (!output) {
1059
+ throw new Error(`Runtime ID output not found for agent '${agentName}'`);
1060
+ }
1061
+
1062
+ return output.OutputValue;
1063
+ } catch (error) {
1064
+ throw new this.serverless.classes.Error(`Failed to get runtime ID: ${error.message}`);
1065
+ }
1066
+ }
1067
+
1068
+ /**
1069
+ * Find the first runtime agent name
1070
+ */
1071
+ getFirstRuntimeAgent() {
1072
+ const agents = this.getAgentsConfig();
1073
+ if (!agents) {
1074
+ return null;
1075
+ }
1076
+
1077
+ for (const [name, config] of Object.entries(agents)) {
1078
+ if (config.type === 'runtime') {
1079
+ return name;
1080
+ }
1081
+ }
1082
+
1083
+ return null;
1084
+ }
1085
+
1086
+ /**
1087
+ * Invoke a deployed AgentCore runtime agent
1088
+ */
1089
+ async invokeAgent() {
1090
+ const { spawnSync } = require('child_process');
1091
+
1092
+ // Determine which agent to invoke
1093
+ let agentName = this.options.agent;
1094
+ if (!agentName) {
1095
+ agentName = this.getFirstRuntimeAgent();
1096
+ if (!agentName) {
1097
+ throw new this.serverless.classes.Error('No runtime agents found in configuration');
1098
+ }
1099
+ }
1100
+
1101
+ const message = this.options.message;
1102
+ if (!message) {
1103
+ throw new this.serverless.classes.Error(
1104
+ 'Message is required. Use --message or -m to specify'
1105
+ );
1106
+ }
1107
+
1108
+ this.log.info(`Invoking agent: ${agentName}`);
1109
+
1110
+ // Get the runtime ARN from stack outputs
1111
+ const runtimeArn = await this.getRuntimeArn(agentName);
1112
+ const region = this.provider.getRegion();
1113
+
1114
+ // Generate or use provided session ID (must be at least 33 characters)
1115
+ const crypto = require('crypto');
1116
+ const sessionId = this.options.session || `session-${Date.now()}-${crypto.randomUUID()}`;
1117
+
1118
+ // Create the payload and base64 encode it (required by AWS CLI)
1119
+ const payload = JSON.stringify({
1120
+ prompt: message,
1121
+ });
1122
+ const payloadBase64 = Buffer.from(payload).toString('base64');
1123
+
1124
+ const fs = require('fs');
1125
+ const os = require('os');
1126
+ const path = require('path');
1127
+
1128
+ const outputFile = path.join(os.tmpdir(), `agentcore-response-${Date.now()}.bin`);
1129
+
1130
+ try {
1131
+ this.log.info(`Session ID: ${sessionId}`);
1132
+ this.log.info('Sending request...');
1133
+ this.log.notice('');
1134
+
1135
+ // Use spawnSync with array arguments to avoid shell injection
1136
+ const args = [
1137
+ 'bedrock-agentcore',
1138
+ 'invoke-agent-runtime',
1139
+ '--agent-runtime-arn',
1140
+ runtimeArn,
1141
+ '--payload',
1142
+ payloadBase64,
1143
+ '--content-type',
1144
+ 'application/json',
1145
+ '--accept',
1146
+ 'application/json',
1147
+ '--runtime-session-id',
1148
+ sessionId,
1149
+ '--region',
1150
+ region,
1151
+ outputFile,
1152
+ ];
1153
+
1154
+ const result = spawnSync('aws', args, { stdio: 'inherit' });
1155
+
1156
+ if (result.error) {
1157
+ throw result.error;
1158
+ }
1159
+ if (result.status !== 0) {
1160
+ throw new Error(`AWS CLI exited with code ${result.status}`);
1161
+ }
1162
+
1163
+ // Read and display the response
1164
+ if (fs.existsSync(outputFile)) {
1165
+ const response = fs.readFileSync(outputFile, 'utf-8');
1166
+ this.log.notice('Response:');
1167
+ this.log.notice('─'.repeat(50));
1168
+
1169
+ try {
1170
+ // Try to parse and pretty print JSON
1171
+ const parsed = JSON.parse(response);
1172
+ this.log.notice(JSON.stringify(parsed, null, 2));
1173
+ } catch {
1174
+ // If not JSON, print raw
1175
+ this.log.notice(response);
1176
+ }
1177
+
1178
+ this.log.notice('─'.repeat(50));
1179
+ }
1180
+ } finally {
1181
+ // Cleanup temp files
1182
+ if (fs.existsSync(outputFile)) {
1183
+ fs.unlinkSync(outputFile);
1184
+ }
1185
+ }
1186
+ }
1187
+
1188
+ /**
1189
+ * Parse a time string like "1h", "30m", "5m" to milliseconds ago
1190
+ */
1191
+ parseTimeAgo(timeStr) {
1192
+ if (!timeStr) {
1193
+ return Date.now() - 60 * 60 * 1000;
1194
+ } // Default 1 hour
1195
+
1196
+ const match = timeStr.match(/^(\d+)([mhd])$/);
1197
+ if (match) {
1198
+ const value = parseInt(match[1], 10);
1199
+ const unit = match[2];
1200
+ const multipliers = { m: 60 * 1000, h: 60 * 60 * 1000, d: 24 * 60 * 60 * 1000 };
1201
+
1202
+ return Date.now() - value * multipliers[unit];
1203
+ }
1204
+
1205
+ // Try parsing as date
1206
+ const date = new Date(timeStr);
1207
+ if (!isNaN(date.getTime())) {
1208
+ return date.getTime();
1209
+ }
1210
+
1211
+ return Date.now() - 60 * 60 * 1000; // Default 1 hour
1212
+ }
1213
+
1214
+ /**
1215
+ * Fetch logs for a deployed AgentCore runtime
1216
+ */
1217
+ async fetchLogs() {
1218
+ const { spawnSync, spawn } = require('child_process');
1219
+
1220
+ // Determine which agent to get logs for
1221
+ let agentName = this.options.agent;
1222
+ if (!agentName) {
1223
+ agentName = this.getFirstRuntimeAgent();
1224
+ if (!agentName) {
1225
+ throw new this.serverless.classes.Error('No runtime agents found in configuration');
1226
+ }
1227
+ }
1228
+
1229
+ this.log.info(`Fetching logs for agent: ${agentName}`);
1230
+
1231
+ // Get the runtime ID
1232
+ const runtimeId = await this.getRuntimeId(agentName);
1233
+ const region = this.provider.getRegion();
1234
+
1235
+ // Log group pattern for AgentCore runtimes
1236
+ // Format: /aws/bedrock-agentcore/runtimes/<agent_id>-<endpoint_name>/[runtime-logs]
1237
+ const logGroupPrefix = `/aws/bedrock-agentcore/runtimes/${runtimeId}`;
1238
+
1239
+ this.log.info(`Log group prefix: ${logGroupPrefix}`);
1240
+
1241
+ // First, list log groups matching the prefix
1242
+ try {
1243
+ // Use spawnSync with array arguments to avoid shell injection
1244
+ const listArgs = [
1245
+ 'logs',
1246
+ 'describe-log-groups',
1247
+ '--log-group-name-prefix',
1248
+ logGroupPrefix,
1249
+ '--region',
1250
+ region,
1251
+ '--output',
1252
+ 'json',
1253
+ ];
1254
+
1255
+ const listResult = spawnSync('aws', listArgs, { encoding: 'utf-8' });
1256
+ if (listResult.error) {
1257
+ throw listResult.error;
1258
+ }
1259
+ if (listResult.status !== 0) {
1260
+ throw new Error(listResult.stderr || `AWS CLI exited with code ${listResult.status}`);
1261
+ }
1262
+ const logGroups = JSON.parse(listResult.stdout);
1263
+
1264
+ if (!logGroups.logGroups || logGroups.logGroups.length === 0) {
1265
+ this.log.notice('No log groups found. The agent may not have been invoked yet.');
1266
+ this.log.notice(`Looking for: ${logGroupPrefix}*`);
1267
+
1268
+ return;
1269
+ }
1270
+
1271
+ // Use the first log group found (runtime-logs)
1272
+ const logGroupName = logGroups.logGroups[0].logGroupName;
1273
+ this.log.info(`Using log group: ${logGroupName}`);
1274
+
1275
+ if (this.options.tail) {
1276
+ // Stream logs continuously
1277
+ this.log.notice('Streaming logs (Ctrl+C to stop)...');
1278
+ this.log.notice('─'.repeat(50));
1279
+
1280
+ // Use spawn with array arguments (no shell) to avoid injection
1281
+ const tailArgs = [
1282
+ 'logs',
1283
+ 'tail',
1284
+ logGroupName,
1285
+ '--follow',
1286
+ '--region',
1287
+ region,
1288
+ ...(this.options.filter ? ['--filter-pattern', this.options.filter] : []),
1289
+ ];
1290
+
1291
+ const tailCmd = spawn('aws', tailArgs, { stdio: 'inherit' });
1292
+
1293
+ // Handle graceful shutdown
1294
+ process.on('SIGINT', () => {
1295
+ tailCmd.kill();
1296
+ process.exit(0);
1297
+ });
1298
+
1299
+ await new Promise((resolve) => {
1300
+ tailCmd.on('close', resolve);
1301
+ });
1302
+ } else {
1303
+ // Fetch recent logs
1304
+ const startTime = this.parseTimeAgo(this.options.startTime);
1305
+
1306
+ // Use spawnSync with array arguments to avoid shell injection
1307
+ const logsArgs = [
1308
+ 'logs',
1309
+ 'filter-log-events',
1310
+ '--log-group-name',
1311
+ logGroupName,
1312
+ '--start-time',
1313
+ startTime.toString(),
1314
+ '--region',
1315
+ region,
1316
+ ...(this.options.filter ? ['--filter-pattern', this.options.filter] : []),
1317
+ '--output',
1318
+ 'json',
1319
+ ];
1320
+
1321
+ const logsResult = spawnSync('aws', logsArgs, { encoding: 'utf-8' });
1322
+ if (logsResult.error) {
1323
+ throw logsResult.error;
1324
+ }
1325
+ if (logsResult.status !== 0) {
1326
+ throw new Error(logsResult.stderr || `AWS CLI exited with code ${logsResult.status}`);
1327
+ }
1328
+ const events = JSON.parse(logsResult.stdout);
1329
+
1330
+ if (!events.events || events.events.length === 0) {
1331
+ this.log.notice('No log events found in the specified time range.');
1332
+ this.log.notice(`Time range: since ${new Date(startTime).toISOString()}`);
1333
+
1334
+ return;
1335
+ }
1336
+
1337
+ this.log.notice(`Found ${events.events.length} log events:`);
1338
+ this.log.notice('─'.repeat(50));
1339
+
1340
+ for (const event of events.events) {
1341
+ const timestamp = new Date(event.timestamp).toISOString();
1342
+ const message = event.message.trim();
1343
+ this.log.notice(`[${timestamp}] ${message}`);
1344
+ }
1345
+
1346
+ this.log.notice('─'.repeat(50));
1347
+ }
1348
+ } catch (error) {
1349
+ throw new this.serverless.classes.Error(`Failed to fetch logs: ${error.message}`);
1350
+ }
1351
+ }
1352
+ }
1353
+
1354
+ module.exports = ServerlessBedrockAgentCore;