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.
@@ -0,0 +1,168 @@
1
+ 'use strict';
2
+
3
+ const { getResourceName, getLogicalId } = require('../utils/naming');
4
+
5
+ // Track if deprecation warning has been shown (module-level to show only once)
6
+ let deprecationWarningShown = false;
7
+
8
+ /**
9
+ * Reset the deprecation warning flag (for testing purposes)
10
+ */
11
+ function resetDeprecationWarning() {
12
+ deprecationWarningShown = false;
13
+ }
14
+
15
+ /**
16
+ * Detect if strategy is in legacy format
17
+ * Legacy format has flat structure with type/name at top level
18
+ * New format uses typed union like SemanticMemoryStrategy, etc.
19
+ *
20
+ * @param {Object} strategy - The strategy configuration
21
+ * @returns {boolean} True if legacy format
22
+ */
23
+ function isLegacyFormat(strategy) {
24
+ // New format has one of these as top-level keys
25
+ const newFormatKeys = [
26
+ 'SemanticMemoryStrategy',
27
+ 'SummaryMemoryStrategy',
28
+ 'UserPreferenceMemoryStrategy',
29
+ 'CustomMemoryStrategy',
30
+ ];
31
+
32
+ // If any new format key exists, it's new format
33
+ if (newFormatKeys.some((key) => strategy[key] !== undefined)) {
34
+ return false;
35
+ }
36
+
37
+ // If it has 'type' property at top level, it's legacy format
38
+ return strategy.type !== undefined;
39
+ }
40
+
41
+ /**
42
+ * Convert legacy strategy format to new typed union format
43
+ *
44
+ * @param {Object} strategy - Legacy format strategy
45
+ * @returns {Object} New format strategy
46
+ */
47
+ function convertLegacyStrategy(strategy) {
48
+ const baseConfig = {
49
+ Name: strategy.name,
50
+ ...(strategy.description && { Description: strategy.description }),
51
+ ...(strategy.namespaces && { Namespaces: strategy.namespaces }),
52
+ };
53
+
54
+ const type = (strategy.type || '').toLowerCase();
55
+
56
+ switch (type) {
57
+ case 'semantic':
58
+ return {
59
+ SemanticMemoryStrategy: {
60
+ ...baseConfig,
61
+ // Note: Managed strategies don't require explicit Type or model configuration
62
+ },
63
+ };
64
+
65
+ case 'summary':
66
+ case 'summarization':
67
+ return {
68
+ SummaryMemoryStrategy: {
69
+ ...baseConfig,
70
+ // Note: Managed strategies don't require explicit Type
71
+ },
72
+ };
73
+
74
+ case 'userpreference':
75
+ case 'user_preference':
76
+ return {
77
+ UserPreferenceMemoryStrategy: {
78
+ ...baseConfig,
79
+ // Note: Managed strategies don't require explicit Type
80
+ },
81
+ };
82
+
83
+ case 'custom':
84
+ return {
85
+ CustomMemoryStrategy: {
86
+ ...baseConfig,
87
+ ...(strategy.configuration && { Configuration: strategy.configuration }),
88
+ },
89
+ };
90
+
91
+ default:
92
+ throw new Error(`Unknown memory strategy type: ${type}`);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Build memory strategies configuration
98
+ * Supports both legacy flat format and new typed union format
99
+ *
100
+ * @param {Array} strategies - The strategies configuration from serverless.yml
101
+ * @returns {Array|null} CloudFormation memory strategies or null
102
+ */
103
+ function buildMemoryStrategies(strategies) {
104
+ if (!strategies || strategies.length === 0) {
105
+ return null;
106
+ }
107
+
108
+ return strategies.map((strategy) => {
109
+ if (isLegacyFormat(strategy)) {
110
+ // Emit deprecation warning once per deployment
111
+ if (!deprecationWarningShown) {
112
+ console.warn(
113
+ '\x1b[33m%s\x1b[0m', // Yellow color
114
+ 'DEPRECATION WARNING: Memory strategy format has changed to typed union structure. ' +
115
+ 'Please update your configuration. See documentation for migration guide.'
116
+ );
117
+ deprecationWarningShown = true;
118
+ }
119
+
120
+ return convertLegacyStrategy(strategy);
121
+ }
122
+
123
+ // Already in new format - pass through
124
+ return strategy;
125
+ });
126
+ }
127
+
128
+ /**
129
+ * Compile a Memory resource to CloudFormation
130
+ *
131
+ * @param {string} name - The agent name
132
+ * @param {Object} config - The memory configuration
133
+ * @param {Object} context - The compilation context
134
+ * @param {Object} tags - The merged tags
135
+ * @returns {Object} CloudFormation resource definition
136
+ */
137
+ function compileMemory(name, config, context, tags) {
138
+ const { serviceName, stage } = context;
139
+ const resourceName = getResourceName(serviceName, name, stage);
140
+ const roleLogicalId = `${getLogicalId(name, 'Memory')}Role`;
141
+
142
+ const strategies = buildMemoryStrategies(config.strategies);
143
+
144
+ // Default expiry duration to 30 days if not specified
145
+ const eventExpiryDuration = config.eventExpiryDuration || 30;
146
+
147
+ return {
148
+ Type: 'AWS::BedrockAgentCore::Memory',
149
+ Properties: {
150
+ Name: resourceName,
151
+ EventExpiryDuration: eventExpiryDuration,
152
+ ...(config.description && { Description: config.description }),
153
+ ...(config.encryptionKeyArn && { EncryptionKeyArn: config.encryptionKeyArn }),
154
+ ...(!config.roleArn && { MemoryExecutionRoleArn: { 'Fn::GetAtt': [roleLogicalId, 'Arn'] } }),
155
+ ...(config.roleArn && { MemoryExecutionRoleArn: config.roleArn }),
156
+ ...(strategies && { MemoryStrategies: strategies }),
157
+ ...(Object.keys(tags).length > 0 && { Tags: tags }),
158
+ },
159
+ };
160
+ }
161
+
162
+ module.exports = {
163
+ compileMemory,
164
+ buildMemoryStrategies,
165
+ isLegacyFormat,
166
+ convertLegacyStrategy,
167
+ resetDeprecationWarning,
168
+ };
@@ -0,0 +1,234 @@
1
+ 'use strict';
2
+
3
+ const { getResourceName, getLogicalId } = require('../utils/naming');
4
+
5
+ /**
6
+ * Build the artifact configuration for the runtime
7
+ *
8
+ * @param {Object} artifact - The artifact configuration from serverless.yml
9
+ * @returns {Object} CloudFormation artifact configuration
10
+ */
11
+ function buildArtifact(artifact) {
12
+ if (artifact.containerImage) {
13
+ return {
14
+ ContainerConfiguration: {
15
+ ContainerUri: artifact.containerImage,
16
+ },
17
+ };
18
+ }
19
+
20
+ if (artifact.s3) {
21
+ return {
22
+ S3Configuration: {
23
+ S3BucketName: artifact.s3.bucket,
24
+ S3ObjectKey: artifact.s3.key,
25
+ },
26
+ };
27
+ }
28
+
29
+ throw new Error('Artifact must specify either containerImage or s3 configuration');
30
+ }
31
+
32
+ /**
33
+ * Build network configuration for the runtime
34
+ *
35
+ * @param {Object} network - The network configuration from serverless.yml
36
+ * @returns {Object} CloudFormation network configuration
37
+ */
38
+ function buildNetworkConfiguration(network = {}) {
39
+ const networkMode = network.networkMode || 'PUBLIC';
40
+
41
+ const config = {
42
+ NetworkMode: networkMode,
43
+ };
44
+
45
+ // Add VPC configuration if VPC mode
46
+ if (networkMode === 'VPC' && network.vpcConfig) {
47
+ config.VpcConfiguration = {
48
+ ...(network.vpcConfig.subnetIds && { SubnetIds: network.vpcConfig.subnetIds }),
49
+ ...(network.vpcConfig.securityGroupIds && {
50
+ SecurityGroupIds: network.vpcConfig.securityGroupIds,
51
+ }),
52
+ };
53
+ }
54
+
55
+ return config;
56
+ }
57
+
58
+ /**
59
+ * Build authorizer configuration for the runtime
60
+ * Supports NONE, AWS_IAM, and CustomJWTAuthorizer
61
+ *
62
+ * @param {Object} authorizer - The authorizer configuration from serverless.yml
63
+ * @returns {Object|null} CloudFormation authorizer configuration or null
64
+ */
65
+ function buildAuthorizerConfiguration(authorizer) {
66
+ if (!authorizer) {
67
+ return null;
68
+ }
69
+
70
+ // Handle CustomJWTAuthorizer - uses OIDC discovery URL
71
+ if (authorizer.customJwtAuthorizer) {
72
+ const jwtConfig = authorizer.customJwtAuthorizer;
73
+
74
+ // DiscoveryUrl is required for CustomJWTAuthorizer
75
+ if (!jwtConfig.discoveryUrl) {
76
+ throw new Error('CustomJWTAuthorizer requires discoveryUrl');
77
+ }
78
+
79
+ return {
80
+ CustomJWTAuthorizer: {
81
+ DiscoveryUrl: jwtConfig.discoveryUrl,
82
+ ...(jwtConfig.allowedAudience && { AllowedAudience: jwtConfig.allowedAudience }),
83
+ ...(jwtConfig.allowedClients && { AllowedClients: jwtConfig.allowedClients }),
84
+ },
85
+ };
86
+ }
87
+
88
+ // Handle legacy format with type field
89
+ if (authorizer.type === 'CUSTOM_JWT' && authorizer.jwtConfiguration) {
90
+ // Convert legacy format to new format
91
+ const discoveryUrl =
92
+ authorizer.jwtConfiguration.discoveryUrl || authorizer.jwtConfiguration.issuer;
93
+
94
+ if (!discoveryUrl) {
95
+ throw new Error('CustomJWTAuthorizer requires discoveryUrl or issuer');
96
+ }
97
+
98
+ return {
99
+ CustomJWTAuthorizer: {
100
+ DiscoveryUrl: discoveryUrl,
101
+ ...(authorizer.jwtConfiguration.allowedAudience && {
102
+ AllowedAudience: authorizer.jwtConfiguration.allowedAudience,
103
+ }),
104
+ ...(authorizer.jwtConfiguration.allowedClients && {
105
+ AllowedClients: authorizer.jwtConfiguration.allowedClients,
106
+ }),
107
+ // Legacy support: audience -> allowedAudience
108
+ ...(authorizer.jwtConfiguration.audience && {
109
+ AllowedAudience: Array.isArray(authorizer.jwtConfiguration.audience)
110
+ ? authorizer.jwtConfiguration.audience
111
+ : [authorizer.jwtConfiguration.audience],
112
+ }),
113
+ },
114
+ };
115
+ }
116
+
117
+ // Return null for NONE or AWS_IAM (handled at runtime level, not in AuthorizerConfiguration)
118
+ return null;
119
+ }
120
+
121
+ /**
122
+ * Build lifecycle configuration for the runtime
123
+ *
124
+ * @param {Object} lifecycle - The lifecycle configuration from serverless.yml
125
+ * @returns {Object|null} CloudFormation lifecycle configuration or null
126
+ */
127
+ function buildLifecycleConfiguration(lifecycle) {
128
+ if (!lifecycle) {
129
+ return null;
130
+ }
131
+
132
+ return {
133
+ ...(lifecycle.idleTimeout !== undefined && { IdleTimeoutSeconds: lifecycle.idleTimeout }),
134
+ ...(lifecycle.maxConcurrency !== undefined && { MaxConcurrency: lifecycle.maxConcurrency }),
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Build protocol configuration for the runtime
140
+ * ProtocolConfiguration is a string enum: HTTP, MCP, or A2A
141
+ *
142
+ * @param {string} protocol - The protocol type (HTTP, MCP, A2A)
143
+ * @returns {string|null} Protocol string or null
144
+ */
145
+ function buildProtocolConfiguration(protocol) {
146
+ if (!protocol) {
147
+ return null;
148
+ }
149
+
150
+ // ProtocolConfiguration is just the protocol string, not an object
151
+ return protocol;
152
+ }
153
+
154
+ /**
155
+ * Build environment variables for the runtime
156
+ *
157
+ * @param {Object} environment - Environment variables object
158
+ * @returns {Object|null} Environment variables or null
159
+ */
160
+ function buildEnvironmentVariables(environment) {
161
+ if (!environment || Object.keys(environment).length === 0) {
162
+ return null;
163
+ }
164
+
165
+ return environment;
166
+ }
167
+
168
+ /**
169
+ * Build request header configuration for the runtime
170
+ * Allows specific headers to be forwarded to the agent
171
+ *
172
+ * @param {Object} requestHeaders - The request headers configuration from serverless.yml
173
+ * @returns {Object|null} CloudFormation RequestHeaderConfiguration or null
174
+ */
175
+ function buildRequestHeaderConfiguration(requestHeaders) {
176
+ if (!requestHeaders || !requestHeaders.allowlist || requestHeaders.allowlist.length === 0) {
177
+ return null;
178
+ }
179
+
180
+ return {
181
+ RequestHeaderAllowlist: requestHeaders.allowlist,
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Compile a Runtime resource to CloudFormation
187
+ *
188
+ * @param {string} name - The agent name
189
+ * @param {Object} config - The runtime configuration
190
+ * @param {Object} context - The compilation context
191
+ * @param {Object} tags - The merged tags
192
+ * @returns {Object} CloudFormation resource definition
193
+ */
194
+ function compileRuntime(name, config, context, tags) {
195
+ const { serviceName, stage } = context;
196
+ const resourceName = getResourceName(serviceName, name, stage);
197
+ const roleLogicalId = `${getLogicalId(name, 'Runtime')}Role`;
198
+
199
+ const artifact = buildArtifact(config.artifact);
200
+ const networkConfig = buildNetworkConfiguration(config.network);
201
+ const authorizerConfig = buildAuthorizerConfiguration(config.authorizer);
202
+ const lifecycleConfig = buildLifecycleConfiguration(config.lifecycle);
203
+ const protocolConfig = buildProtocolConfiguration(config.protocol);
204
+ const envVars = buildEnvironmentVariables(config.environment);
205
+ const requestHeaderConfig = buildRequestHeaderConfiguration(config.requestHeaders);
206
+
207
+ return {
208
+ Type: 'AWS::BedrockAgentCore::Runtime',
209
+ Properties: {
210
+ AgentRuntimeName: resourceName,
211
+ AgentRuntimeArtifact: artifact,
212
+ NetworkConfiguration: networkConfig,
213
+ RoleArn: config.roleArn || { 'Fn::GetAtt': [roleLogicalId, 'Arn'] },
214
+ ...(config.description && { Description: config.description }),
215
+ ...(authorizerConfig && { AuthorizerConfiguration: authorizerConfig }),
216
+ ...(lifecycleConfig && { LifecycleConfiguration: lifecycleConfig }),
217
+ ...(protocolConfig && { ProtocolConfiguration: protocolConfig }),
218
+ ...(envVars && { EnvironmentVariables: envVars }),
219
+ ...(requestHeaderConfig && { RequestHeaderConfiguration: requestHeaderConfig }),
220
+ ...(Object.keys(tags).length > 0 && { Tags: tags }),
221
+ },
222
+ };
223
+ }
224
+
225
+ module.exports = {
226
+ compileRuntime,
227
+ buildArtifact,
228
+ buildNetworkConfiguration,
229
+ buildAuthorizerConfiguration,
230
+ buildLifecycleConfiguration,
231
+ buildProtocolConfiguration,
232
+ buildEnvironmentVariables,
233
+ buildRequestHeaderConfiguration,
234
+ };
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ const { getResourceName } = require('../utils/naming');
4
+
5
+ /**
6
+ * Compile a RuntimeEndpoint resource to CloudFormation
7
+ *
8
+ * @param {string} agentName - The parent agent name
9
+ * @param {string} endpointName - The endpoint name
10
+ * @param {Object} config - The endpoint configuration
11
+ * @param {string} runtimeLogicalId - The logical ID of the parent Runtime resource
12
+ * @param {Object} context - The compilation context
13
+ * @param {Object} tags - The merged tags
14
+ * @returns {Object} CloudFormation resource definition
15
+ */
16
+ function compileRuntimeEndpoint(agentName, endpointName, config, runtimeLogicalId, context, tags) {
17
+ const { serviceName, stage } = context;
18
+
19
+ // Generate endpoint name: {service}_{agent}_{endpoint}_{stage}
20
+ const resourceName = getResourceName(serviceName, `${agentName}_${endpointName}`, stage);
21
+
22
+ return {
23
+ Type: 'AWS::BedrockAgentCore::RuntimeEndpoint',
24
+ DependsOn: [runtimeLogicalId],
25
+ Properties: {
26
+ Name: resourceName,
27
+ AgentRuntimeId: { 'Fn::GetAtt': [runtimeLogicalId, 'AgentRuntimeId'] },
28
+ ...(config.version && { AgentRuntimeVersion: config.version }),
29
+ ...(config.description && { Description: config.description }),
30
+ ...(Object.keys(tags).length > 0 && { Tags: tags }),
31
+ },
32
+ };
33
+ }
34
+
35
+ module.exports = {
36
+ compileRuntimeEndpoint,
37
+ };
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ const { getResourceName } = require('../utils/naming');
4
+
5
+ /**
6
+ * Compile a WorkloadIdentity resource to CloudFormation
7
+ *
8
+ * @param {string} name - The workload identity name
9
+ * @param {Object} config - The workload identity configuration
10
+ * @param {Object} context - The compilation context
11
+ * @param {Object} tags - The merged tags
12
+ * @returns {Object} CloudFormation resource definition
13
+ */
14
+ function compileWorkloadIdentity(name, config, context, tags) {
15
+ const { serviceName, stage } = context;
16
+ // WorkloadIdentity name pattern: [A-Za-z0-9_.-]+
17
+ // Replace underscores with hyphens to match the pattern better
18
+ const resourceName = getResourceName(serviceName, name, stage).replace(/_/g, '-');
19
+
20
+ // Convert tags object to array format for WorkloadIdentity
21
+ // WorkloadIdentity uses Array[Tag] format instead of Object format
22
+ const tagArray = Object.entries(tags).map(([Key, Value]) => ({ Key, Value }));
23
+
24
+ return {
25
+ Type: 'AWS::BedrockAgentCore::WorkloadIdentity',
26
+ Properties: {
27
+ Name: resourceName,
28
+ ...(config.oauth2ReturnUrls && { AllowedResourceOauth2ReturnUrls: config.oauth2ReturnUrls }),
29
+ ...(tagArray.length > 0 && { Tags: tagArray }),
30
+ },
31
+ };
32
+ }
33
+
34
+ module.exports = {
35
+ compileWorkloadIdentity,
36
+ };
@@ -0,0 +1,236 @@
1
+ 'use strict';
2
+
3
+ const { execSync } = require('child_process');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * Execute a shell command synchronously
8
+ */
9
+ function execCommand(command, options = {}) {
10
+ try {
11
+ return execSync(command, {
12
+ encoding: 'utf-8',
13
+ stdio: options.silent ? 'pipe' : 'inherit',
14
+ ...options,
15
+ });
16
+ } catch (error) {
17
+ if (options.ignoreError) {
18
+ return null;
19
+ }
20
+ throw error;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Docker image builder for AgentCore runtimes
26
+ * Follows similar patterns to Serverless Framework's ECR image handling
27
+ */
28
+ class DockerBuilder {
29
+ constructor(serverless, log, progress) {
30
+ this.serverless = serverless;
31
+ this.log = log;
32
+ this.progress = progress;
33
+ this.provider = serverless.getProvider('aws');
34
+ }
35
+
36
+ /**
37
+ * Check if Docker is available
38
+ */
39
+ checkDocker() {
40
+ try {
41
+ execCommand('docker --version', { silent: true });
42
+
43
+ return true;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Get AWS account ID
51
+ */
52
+ async getAccountId() {
53
+ return this.provider.getAccountId();
54
+ }
55
+
56
+ /**
57
+ * Get the region
58
+ */
59
+ getRegion() {
60
+ return this.provider.getRegion();
61
+ }
62
+
63
+ /**
64
+ * Authenticate with ECR
65
+ */
66
+ async ecrLogin(accountId, region) {
67
+ this.log.info('Authenticating with ECR...');
68
+
69
+ const ecrUri = `${accountId}.dkr.ecr.${region}.amazonaws.com`;
70
+
71
+ try {
72
+ // Get ECR login password and pipe to docker login
73
+ const password = execCommand(`aws ecr get-login-password --region ${region}`, {
74
+ silent: true,
75
+ }).trim();
76
+
77
+ execCommand(`docker login --username AWS --password-stdin ${ecrUri}`, {
78
+ input: password,
79
+ silent: true,
80
+ });
81
+
82
+ this.log.info('ECR authentication successful');
83
+
84
+ return ecrUri;
85
+ } catch (error) {
86
+ throw new Error(`ECR login failed: ${error.message}`);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Ensure ECR repository exists, create if not
92
+ */
93
+ async ensureRepository(repositoryName, region) {
94
+ this.log.info(`Checking ECR repository: ${repositoryName}`);
95
+
96
+ try {
97
+ execCommand(
98
+ `aws ecr describe-repositories --repository-names ${repositoryName} --region ${region}`,
99
+ { silent: true }
100
+ );
101
+ this.log.info('Repository exists');
102
+ } catch {
103
+ this.log.info('Creating ECR repository...');
104
+ execCommand(
105
+ `aws ecr create-repository --repository-name ${repositoryName} --region ${region}`,
106
+ { silent: true }
107
+ );
108
+ this.log.info('Repository created');
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Build Docker image
114
+ */
115
+ async buildImage(imageTag, dockerConfig, servicePath) {
116
+ const dockerfile = dockerConfig.file || 'Dockerfile';
117
+ const context = dockerConfig.path || '.';
118
+ // Bedrock AgentCore requires arm64 architecture
119
+ const platform = dockerConfig.platform || 'linux/arm64';
120
+
121
+ const dockerfilePath = path.resolve(servicePath, context, dockerfile);
122
+ const contextPath = path.resolve(servicePath, context);
123
+
124
+ this.log.info(`Building Docker image: ${imageTag}`);
125
+ this.log.info(` Dockerfile: ${dockerfilePath}`);
126
+ this.log.info(` Context: ${contextPath}`);
127
+ this.log.info(` Platform: ${platform}`);
128
+
129
+ // Build the docker command
130
+ let buildCmd = `docker build --platform ${platform} -t ${imageTag}`;
131
+
132
+ // Add build args if specified
133
+ if (dockerConfig.buildArgs) {
134
+ for (const [key, value] of Object.entries(dockerConfig.buildArgs)) {
135
+ buildCmd += ` --build-arg ${key}="${value}"`;
136
+ }
137
+ }
138
+
139
+ // Add cache from if specified
140
+ if (dockerConfig.cacheFrom) {
141
+ for (const cache of dockerConfig.cacheFrom) {
142
+ buildCmd += ` --cache-from ${cache}`;
143
+ }
144
+ }
145
+
146
+ // Add any additional build options
147
+ if (dockerConfig.buildOptions) {
148
+ buildCmd += ` ${dockerConfig.buildOptions.join(' ')}`;
149
+ }
150
+
151
+ buildCmd += ` -f ${dockerfilePath} ${contextPath}`;
152
+
153
+ try {
154
+ execCommand(buildCmd);
155
+ this.log.info('Docker build complete');
156
+ } catch (error) {
157
+ throw new Error(`Docker build failed: ${error.message}`);
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Tag and push image to ECR
163
+ */
164
+ async pushImage(localTag, ecrUri, repositoryName, tag) {
165
+ const ecrImage = `${ecrUri}/${repositoryName}:${tag}`;
166
+
167
+ this.log.info(`Tagging image: ${ecrImage}`);
168
+ execCommand(`docker tag ${localTag} ${ecrImage}`);
169
+
170
+ this.log.info(`Pushing image to ECR...`);
171
+ execCommand(`docker push ${ecrImage}`);
172
+
173
+ this.log.info('Push complete');
174
+
175
+ return ecrImage;
176
+ }
177
+
178
+ /**
179
+ * Build and push image for a runtime agent
180
+ * Returns the ECR image URI to use in CloudFormation
181
+ */
182
+ async buildAndPushForRuntime(agentName, imageConfig, context) {
183
+ const { serviceName, stage, region } = context;
184
+ const accountId = await this.getAccountId();
185
+ const servicePath = this.serverless.serviceDir;
186
+
187
+ // Determine repository name
188
+ const repositoryName = imageConfig.repository || `${serviceName}-${agentName}`;
189
+
190
+ // Determine tag (use stage or specified tag)
191
+ const tag = imageConfig.tag || stage;
192
+
193
+ // Local image tag
194
+ const localTag = `${repositoryName}:${tag}`;
195
+
196
+ // Ensure repository exists
197
+ await this.ensureRepository(repositoryName, region);
198
+
199
+ // Login to ECR
200
+ const ecrUri = await this.ecrLogin(accountId, region);
201
+
202
+ // Build image
203
+ await this.buildImage(localTag, imageConfig, servicePath);
204
+
205
+ // Push to ECR
206
+ const ecrImage = await this.pushImage(localTag, ecrUri, repositoryName, tag);
207
+
208
+ return ecrImage;
209
+ }
210
+
211
+ /**
212
+ * Process all images defined in provider.ecr.images
213
+ * Returns a map of image names to ECR URIs
214
+ */
215
+ async processImages(imagesConfig, context) {
216
+ const imageUris = {};
217
+
218
+ for (const [imageName, imageConfig] of Object.entries(imagesConfig)) {
219
+ // If URI is already specified, use it directly
220
+ if (imageConfig.uri) {
221
+ imageUris[imageName] = imageConfig.uri;
222
+ continue;
223
+ }
224
+
225
+ // Otherwise, build and push
226
+ if (imageConfig.path) {
227
+ const uri = await this.buildAndPushForRuntime(imageName, imageConfig, context);
228
+ imageUris[imageName] = uri;
229
+ }
230
+ }
231
+
232
+ return imageUris;
233
+ }
234
+ }
235
+
236
+ module.exports = { DockerBuilder };