Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import { md5hash } from 'aws-cdk-lib/core/lib/helpers-internal';
import { Construct } from 'constructs';
import { Runtime } from './runtime';
import { ValidationError } from './validation-helpers';
import { Location } from 'aws-cdk-lib/aws-s3';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Stack, Token } from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';

/**
* Abstract base class for agent runtime artifacts.
Expand All @@ -40,6 +44,16 @@ export abstract class AgentRuntimeArtifact {
return new AssetImage(directory, options);
}

/**
* Reference an agent runtime artifact that's constructed directly from an S3 object
* @param s3Location The source code location and configuration details.
* @param runtime The runtime environment for executing the code (for example, Python 3.9 or Node.js 18).
* @param entrypoint The entry point for the code execution, specifying the function or method that should be invoked when the code runs.
*/
public static fromS3(s3Location: Location, runtime: lambda.Runtime, entrypoint: string[]): AgentRuntimeArtifact {
return new S3Image(s3Location, runtime, entrypoint);
}

/**
* Called when the image is used by a Runtime to handle side effects like permissions
*/
Expand Down Expand Up @@ -113,3 +127,42 @@ class AssetImage extends AgentRuntimeArtifact {
} as any;
}
}

class S3Image extends AgentRuntimeArtifact {
private bound = false;

constructor(private readonly s3Location: Location, private readonly runtime: lambda.Runtime, private readonly entrypoint: string[]) {
super();
}

public bind(scope: Construct, runtime: Runtime): void {
// Handle permissions (only once)
if (!this.bound && runtime.role) {
if (!Token.isUnresolved(this.s3Location.bucketName)) {
Stack.of(scope).resolve(this.s3Location.bucketName);
}
const bucket = s3.Bucket.fromBucketName(
scope,
`${this.s3Location.bucketName}CodeArchive`,
this.s3Location.bucketName,
);
// Ensure the policy is applied before the browser resource is created
bucket.grantReadWrite(runtime.role);
this.bound = true;
}
}

public _render(): CfnRuntime.AgentRuntimeArtifactProperty {
return {
code: {
s3: {
bucketName: this.s3Location.bucketName,
objectKey: this.s3Location.objectKey,
versionId: this.s3Location.objectVersion,
},
},
runtime: this.runtime.name,
entrypoint: this.entrypoint,
} as any;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,24 @@ import { RuntimeAuthorizerConfiguration } from './runtime-authorizer-configurati
import { RuntimeBase, IBedrockAgentRuntime, AgentRuntimeAttributes } from './runtime-base';
import { RuntimeEndpoint } from './runtime-endpoint';
import { RuntimeNetworkConfiguration } from '../network/network-configuration';
import { ProtocolType } from './types';
import { LifecycleConfiguration, ProtocolType, RequestHeaderConfiguration } from './types';
import { validateStringField, ValidationError, validateFieldPattern } from './validation-helpers';

/******************************************************************************
* Constants
*****************************************************************************/
/**
* Minimum timeout for idle runtime sessions
* @internal
*/
const LIFECYCLE_MIN_TIMEOUT = Duration.seconds(60);

/**
* Maximum lifetime for the instance
* @internal
*/
const LIFECYCLE_MAX_LIFETIME = Duration.seconds(28800);

/******************************************************************************
* Props
*****************************************************************************/
Expand Down Expand Up @@ -93,6 +108,18 @@ export interface RuntimeProps {
* @default {} - no tags
*/
readonly tags?: { [key: string]: string };

/**
* Configuration for HTTP request headers that will be passed through to the runtime.
* @default - No request headers configured
*/
readonly requestHeaderConfiguration?: RequestHeaderConfiguration;

/**
* The life cycle configuration for the AgentCore Runtime.
* @default - No lifecycle configuration
*/
readonly lifecycleConfiguration?: LifecycleConfiguration;
}

/**
Expand Down Expand Up @@ -226,6 +253,7 @@ export class Runtime extends RuntimeBase {
private readonly networkConfiguration: RuntimeNetworkConfiguration ;
private readonly protocolConfiguration: ProtocolType;
private readonly authorizerConfiguration?: RuntimeAuthorizerConfiguration;
private readonly lifecycleConfiguration?: LifecycleConfiguration;

constructor(scope: Construct, id: string, props: RuntimeProps) {
super(scope, id);
Expand All @@ -249,6 +277,16 @@ export class Runtime extends RuntimeBase {
this.validateTags(props.tags);
}

if (props.requestHeaderConfiguration) {
this.validateRequestHeaderConfiguration(props.requestHeaderConfiguration);
}

this.lifecycleConfiguration = {
idleRuntimeSessionTimeout: props.lifecycleConfiguration?.idleRuntimeSessionTimeout ?? LIFECYCLE_MIN_TIMEOUT,
maxLifetime: props.lifecycleConfiguration?.maxLifetime ?? LIFECYCLE_MAX_LIFETIME,
};
this.validateLifecycleConfiguration(this.lifecycleConfiguration);

if (props.executionRole) {
this.role = props.executionRole;
if (!Token.isUnresolved(props.executionRole.roleArn)) {
Expand Down Expand Up @@ -292,6 +330,8 @@ export class Runtime extends RuntimeBase {
produce: () => this.renderEnvironmentVariables(props.environmentVariables),
}),
tags: props.tags ?? {},
requestHeaderConfiguration: this.renderRequestHeaderConfiguration(props.requestHeaderConfiguration),
lifecycleConfiguration: this.renderLifecycleConfiguration(),
};
if (props.authorizerConfiguration) {
cfnProps.authorizerConfiguration = Lazy.any({
Expand Down Expand Up @@ -398,17 +438,102 @@ export class Runtime extends RuntimeBase {
// set permission with bind
this.agentRuntimeArtifact.bind(this, this);
const config = this.agentRuntimeArtifact._render();
const containerUri = (config as any).containerUri;
if (containerUri) {
this.validateContainerUri(containerUri);
if ((config as any).code) { // S3Image
return {
codeConfiguration: config,
};
} else {
const containerUri = (config as any).containerUri;
if (containerUri) {
this.validateContainerUri(containerUri);
}
return {
containerConfiguration: {
containerUri: containerUri,
},
};
}
}

/**
* Renders the request header configuration for CloudFormation
* @internal
*/
private renderRequestHeaderConfiguration(requestHeaderConfiguration?: RequestHeaderConfiguration): any {
if (!requestHeaderConfiguration?.allowList) {
return undefined;
}
return {
containerConfiguration: {
containerUri: containerUri,
},
allowList: requestHeaderConfiguration.allowList,
};
}

/**
* Renders the lifecycle configuration for CloudFormation
* @internal
*/
private renderLifecycleConfiguration(): any {
return {
idleRuntimeSessionTimeout: this.lifecycleConfiguration?.idleRuntimeSessionTimeout?.toSeconds(),
maxLifetime: this.lifecycleConfiguration?.maxLifetime?.toSeconds(),
};
}

/**
* Validates the request header configuration
* @throws Error if validation fails
*/
private validateRequestHeaderConfiguration(requestHeaderConfiguration: RequestHeaderConfiguration): void {
const allErrors: string[] = [];
if (requestHeaderConfiguration.allowList) {
if (requestHeaderConfiguration.allowList.length < 1 || requestHeaderConfiguration.allowList.length > 20) {
allErrors.push('Request header allow list contain between 1 and 20 headers');
}

for (const header of requestHeaderConfiguration.allowList) {

// Validate length
const lengthErrors = validateStringField({
value: header,
fieldName: 'Request header',
minLength: 1,
maxLength: 256,
});
allErrors.push(...lengthErrors);

const patternErrors = validateFieldPattern(
header,
'Request header',
/(Authorization|X-Amzn-Bedrock-AgentCore-Runtime-Custom-[a-zA-Z0-9-]+)/,
'Request header must contain only letters, numbers, and hyphens',
);
allErrors.push(...patternErrors);
}
}
if (allErrors.length > 0) {
throw new ValidationError(allErrors.join('\n'));
}
}

/**
* Validates the lifecycle configuration
* @throws Error if validation fails
*/
private validateLifecycleConfiguration(lifecycleConfiguration: LifecycleConfiguration): void {
if (lifecycleConfiguration.idleRuntimeSessionTimeout) {
if (lifecycleConfiguration.idleRuntimeSessionTimeout.toSeconds() < LIFECYCLE_MIN_TIMEOUT.toSeconds()
|| lifecycleConfiguration.idleRuntimeSessionTimeout.toSeconds() > LIFECYCLE_MAX_LIFETIME.toSeconds()) {
throw new ValidationError(`Idle runtime session timeout must be between ${LIFECYCLE_MIN_TIMEOUT.toSeconds()} seconds and ${LIFECYCLE_MAX_LIFETIME.toSeconds()} seconds`);
}
}
if (lifecycleConfiguration.maxLifetime) {
if (lifecycleConfiguration.maxLifetime.toSeconds() < LIFECYCLE_MAX_LIFETIME.toSeconds()
|| lifecycleConfiguration.maxLifetime.toSeconds() < LIFECYCLE_MIN_TIMEOUT.toSeconds()) {
throw new ValidationError(`Maximum lifetime must be between ${LIFECYCLE_MIN_TIMEOUT.toSeconds()} seconds and ${LIFECYCLE_MAX_LIFETIME.toSeconds()} seconds`);
}
}
}

/**
* Validates the runtime name format
* Pattern: ^[a-zA-Z][a-zA-Z0-9_]{0,47}$
Expand All @@ -425,7 +550,7 @@ export class Runtime extends RuntimeBase {
value: this.agentRuntimeName,
fieldName: 'Runtime name',
minLength: 1,
maxLength: 48,
maxLength: 256,
});

// Validate pattern
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
* Enums
*****************************************************************************/

import { Duration } from "aws-cdk-lib";

/**
* Protocol configuration for Agent Runtime
*/
Expand All @@ -22,3 +24,35 @@ export enum ProtocolType {
*/
A2A = 'A2A',
}

/**
* Configuration for HTTP request headers that will be passed through to the runtime.
*/
export interface RequestHeaderConfiguration {
/**
* A list of HTTP request headers that are allowed to be passed through to the runtime.
* @default - No request headers allowed
*/
readonly allowList?: string[];
}

/**
* LifecycleConfiguration lets you manage the lifecycle of runtime sessions and resources in AgentCore Runtime.
* This configuration helps optimize resource utilization by automatically cleaning up idle sessions and preventing
* long-running instances from consuming resources indefinitely.
*/
export interface LifecycleConfiguration {
/**
* Timeout in seconds for idle runtime sessions. When a session remains idle for this duration,
* it will be automatically terminated. Default: 900 seconds (15 minutes).
* @default - 900 seconds (15 minutes)
*/
readonly idleRuntimeSessionTimeout?: Duration;

/**
* Maximum lifetime for the instance in seconds. Once reached, instances will be automatically
* terminated and replaced. Default: 28800 seconds (8 hours).
* @default - 28800 seconds (8 hours)
*/
readonly maxLifetime?: Duration;
}
Loading
Loading