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/CHANGELOG.md +52 -0
- package/LICENSE +21 -0
- package/README.md +476 -0
- package/package.json +96 -0
- package/src/compilers/browser.js +110 -0
- package/src/compilers/codeInterpreter.js +64 -0
- package/src/compilers/gateway.js +98 -0
- package/src/compilers/gatewayTarget.js +285 -0
- package/src/compilers/index.js +15 -0
- package/src/compilers/memory.js +168 -0
- package/src/compilers/runtime.js +234 -0
- package/src/compilers/runtimeEndpoint.js +37 -0
- package/src/compilers/workloadIdentity.js +36 -0
- package/src/docker/builder.js +236 -0
- package/src/iam/policies.js +591 -0
- package/src/index.js +1354 -0
- package/src/utils/index.js +9 -0
- package/src/utils/naming.js +79 -0
- package/src/utils/tags.js +56 -0
- package/src/validators/schema.js +250 -0
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;
|