From 4dbd6bbd72d698ce39d645c0150d80daf8cfc49a Mon Sep 17 00:00:00 2001 From: EC2 Default User Date: Tue, 19 May 2026 04:33:38 +0000 Subject: [PATCH 1/4] Add Codex CLI with OpenAI API key example on AgentCore Runtime --- .../Dockerfile | 34 ++ .../README.md | 155 ++++++++ .../cfn-vpc.yaml | 371 ++++++++++++++++++ .../cleanup.py | 119 ++++++ .../cleanup.sh | 15 + .../codex-config.toml | 2 + .../deploy.py | 364 +++++++++++++++++ .../entrypoint.sh | 15 + .../exec_cmd.py | 112 ++++++ .../invoke.py | 130 ++++++ .../server.js | 88 +++++ .../setup.sh | 136 +++++++ .../update.py | 153 ++++++++ 13 files changed, 1694 insertions(+) create mode 100644 01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/Dockerfile create mode 100644 01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/README.md create mode 100644 01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/cfn-vpc.yaml create mode 100644 01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/cleanup.py create mode 100755 01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/cleanup.sh create mode 100644 01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/codex-config.toml create mode 100644 01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/deploy.py create mode 100644 01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/entrypoint.sh create mode 100644 01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/exec_cmd.py create mode 100644 01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/invoke.py create mode 100644 01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/server.js create mode 100755 01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/setup.sh create mode 100644 01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/update.py diff --git a/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/Dockerfile b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/Dockerfile new file mode 100644 index 000000000..18f287f59 --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/Dockerfile @@ -0,0 +1,34 @@ +FROM node:22-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + git curl ca-certificates jq awscli && \ + rm -rf /var/lib/apt/lists/* + +# Remove existing node user and create agent user with uid 1000 (matches S3 Files access point) +RUN userdel -r node && \ + useradd -m -d /home/agent -u 1000 agent && \ + mkdir -p /home/agent/.codex /home/agent/.npm-global /app && \ + chown -R agent:agent /home/agent /app + +# Switch to agent user for all subsequent steps +USER agent + +ENV NPM_CONFIG_PREFIX=/home/agent/.npm-global +ENV PATH=/home/agent/.npm-global/bin:$PATH + +# Install codex as agent user so all files are owned correctly +RUN npm install -g @openai/codex + +# Initialize a git repo so codex treats /home/agent as a trusted directory +RUN git config --global user.name "AgentCore Bot" && \ + git config --global user.email "agentcore@company.com" && \ + git -C /home/agent init + +# Codex config pointing at OpenAI models +COPY --chown=agent:agent codex-config.toml /home/agent/.codex/config.toml +COPY --chown=agent:agent server.js /app/server.js +COPY --chown=agent:agent entrypoint.sh /app/entrypoint.sh + +WORKDIR /app +EXPOSE 8080 +CMD ["/bin/bash", "/app/entrypoint.sh"] diff --git a/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/README.md b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/README.md new file mode 100644 index 000000000..04a472c26 --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/README.md @@ -0,0 +1,155 @@ +# Codex CLI on AgentCore Runtime with S3 Files + +Deploys Codex CLI as an HTTP agent on AWS Bedrock AgentCore Runtime, with an S3 Files file system mounted at `/mnt/s3files` for persistent storage shared across sessions. + +## Architecture + +``` + ┌─────────────────────────┐ ┌─────────────────────────┐ + │ AgentCore Runtime │ │ AgentCore Runtime │ + │ Session A │ │ Session B │ + │ (Codex CLI) │ │ (Codex CLI) │ + │ │ │ │ + │ /mnt/s3files ──────────┼────┐ │ /mnt/s3files ──────────┼────┐ + └─────────────────────────┘ │ └─────────────────────────┘ │ + │ │ + ▼ ▼ + ┌──────────────────────────────────────────────────┐ + │ S3 Files File System │ + │ │ + │ ┌────────────────────────┐ │ + │ │ S3 Files Access Point │ │ + │ │ (uid/gid 1000) │ │ + │ └───────────┬────────────┘ │ + └──────────────┼───────────────────────────────────┘ + │ + ▼ + ┌──────────────────────────────┐ + │ S3 Bucket │ + │ (agentcore-) │ + │ │ + │ agents/ │ + │ ├── skills/ │ + │ ├── results/ │ + │ └── ... │ + └──────────────────────────────┘ +``` + +Multiple runtime sessions mount the same S3 Files file system, enabling agents to share skills, results, and data across independent invocations. + +``` +CloudFormation stack (cfn-vpc.yaml): + VPC, subnets, NAT Gateway, Security Group + S3 Files IAM role, file system, access point, mount targets + +deploy.py creates: + IAM execution role + AgentCore Runtime (container from ECR, S3 Files mounted at /mnt/s3files) +``` + +## Prerequisites + +### Python environment + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +uv venv --python 3.13 .venv +source .venv/bin/activate +uv pip install boto3 awscli --force-reinstall --no-cache-dir + +# Install Docker for Amazon Linux 2023 as example +sudo dnf install -y docker +# Start and enable Docker service +sudo systemctl start docker +sudo systemctl enable docker + +# Add your user to the docker group +sudo usermod -aG docker $USER +# Apply group change without logging out +newgrp docker + +#Verify Docker works +docker --version +#Set up buildx for multi-platform builds +docker buildx create --use +``` + + + +### S3 Files IAM policies + +The CloudFormation stack creates an IAM role (`S3FilesRole`) with the permissions required by S3 Files (S3, KMS, and EventBridge). For the full list of required policies, see the [S3 Files prerequisite policies](https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-prereq-policies.html) documentation. + +## Step-by-step guide + +### Step 1 — Infrastructure setup (CloudFormation) + +Run the setup script to create the S3 bucket, deploy the CloudFormation stack (VPC, subnets, NAT Gateway, Security Group, S3 Files), build the arm64 Docker image, and push it to ECR. + +```bash +./setup.sh us-west-2 +``` + +All outputs are saved to `envvars.config` and used automatically by the next steps. + +### Step 2 — Deploy the agent + +Create the IAM execution role and the AgentCore Runtime: + +```bash +python deploy.py +``` + +The script waits until the runtime status is `READY` and saves the runtime config to `runtime_config.json`. + +If you need to update an existing runtime (e.g. after rebuilding the Docker image), run: + +```bash +python update.py +``` + +### Step 3 — Invoke the agent + +Send a prompt to the deployed agent. The first call creates a new session; subsequent calls can reuse the session ID for conversation continuity. + +**Session A** — create a shared skill on the persistent filesystem: + +```bash +python invoke.py "can u create a new skill, to review python code? This skill should be created into /mnt/s3files/skills/" +``` + +Continue the conversation within the same session: + +```bash +python invoke.py --session "now add unit tests for that skill" +``` + +**Session B** — a completely new session accesses the same filesystem and uses the skill created by Session A: + +```bash +python invoke.py "list the skills available in /mnt/s3files/skills/ and use the python review skill to review this code: def add(a,b): return a+b" +``` + +Both sessions share `/mnt/s3files`, so anything written by one session is immediately available to others. + +### Step 4 — Execute a command on the running session + +Run a shell command directly on the container using the session ID from the previous step: + +```bash +python exec_cmd.py --session 7fd93a80-8838-4721-abea-b1787dd0172c "ls -l /mnt/s3files" +``` + +### Step 5 — Cleanup + +Delete all AgentCore resources (runtime, IAM role) and the CloudFormation stack. The S3 bucket is kept. + +```bash +python cleanup.py +``` + +Or use the shell wrapper: + +```bash +./cleanup.sh +``` diff --git a/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/cfn-vpc.yaml b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/cfn-vpc.yaml new file mode 100644 index 000000000..af973dde2 --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/cfn-vpc.yaml @@ -0,0 +1,371 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: VPC infrastructure for AgentCore Claude Code with S3 Files + +Parameters: + VpcCidr: + Type: String + Default: "10.0.0.0/16" + BucketName: + Type: String + Description: S3 bucket name (agentcore-) + S3FilesPrefix: + Type: String + Default: "agents/" + +Mappings: + AgentCoreAZs: + us-east-1: + AZ1: use1-az1 + AZ2: use1-az2 + us-east-2: + AZ1: use2-az1 + AZ2: use2-az2 + us-west-2: + AZ1: usw2-az1 + AZ2: usw2-az2 + eu-west-1: + AZ1: euw1-az1 + AZ2: euw1-az2 + eu-central-1: + AZ1: euc1-az1 + AZ2: euc1-az2 + eu-north-1: + AZ1: eun1-az1 + AZ2: eun1-az2 + eu-west-2: + AZ1: euw2-az1 + AZ2: euw2-az2 + eu-west-3: + AZ1: euw3-az1 + AZ2: euw3-az2 + ap-southeast-1: + AZ1: apse1-az1 + AZ2: apse1-az2 + ap-southeast-2: + AZ1: apse2-az1 + AZ2: apse2-az2 + ap-northeast-1: + AZ1: apne1-az1 + AZ2: apne1-az2 + ap-northeast-2: + AZ1: apne2-az1 + AZ2: apne2-az2 + ap-south-1: + AZ1: aps1-az1 + AZ2: aps1-az2 + ca-central-1: + AZ1: cac1-az1 + AZ2: cac1-az2 + +Resources: + + # ── VPC ────────────────────────────────────────────────────────────────────── + + VPC: + Type: AWS::EC2::VPC + Properties: + CidrBlock: !Ref VpcCidr + EnableDnsSupport: true + EnableDnsHostnames: true + Tags: + - Key: Name + Value: agentcore-vpc + + InternetGateway: + Type: AWS::EC2::InternetGateway + Properties: + Tags: + - Key: Name + Value: agentcore-igw + + VPCGatewayAttachment: + Type: AWS::EC2::VPCGatewayAttachment + Properties: + VpcId: !Ref VPC + InternetGatewayId: !Ref InternetGateway + + # ── Public Subnets ────────────────────────────────────────────────────────── + + PublicSubnet1: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + CidrBlock: "10.0.1.0/24" + AvailabilityZoneId: !FindInMap [AgentCoreAZs, !Ref "AWS::Region", AZ1] + MapPublicIpOnLaunch: true + Tags: + - Key: Name + Value: agentcore-public-1 + + PublicSubnet2: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + CidrBlock: "10.0.2.0/24" + AvailabilityZoneId: !FindInMap [AgentCoreAZs, !Ref "AWS::Region", AZ2] + MapPublicIpOnLaunch: true + Tags: + - Key: Name + Value: agentcore-public-2 + + # ── Private Subnets ───────────────────────────────────────────────────────── + + PrivateSubnet1: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + CidrBlock: "10.0.11.0/24" + AvailabilityZoneId: !FindInMap [AgentCoreAZs, !Ref "AWS::Region", AZ1] + Tags: + - Key: Name + Value: agentcore-private-1 + + PrivateSubnet2: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + CidrBlock: "10.0.12.0/24" + AvailabilityZoneId: !FindInMap [AgentCoreAZs, !Ref "AWS::Region", AZ2] + Tags: + - Key: Name + Value: agentcore-private-2 + + # ── Public Route Table ────────────────────────────────────────────────────── + + PublicRouteTable: + Type: AWS::EC2::RouteTable + Properties: + VpcId: !Ref VPC + Tags: + - Key: Name + Value: agentcore-public-rt + + PublicRoute: + Type: AWS::EC2::Route + DependsOn: VPCGatewayAttachment + Properties: + RouteTableId: !Ref PublicRouteTable + DestinationCidrBlock: "0.0.0.0/0" + GatewayId: !Ref InternetGateway + + PublicSubnet1RTAssoc: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + SubnetId: !Ref PublicSubnet1 + RouteTableId: !Ref PublicRouteTable + + PublicSubnet2RTAssoc: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + SubnetId: !Ref PublicSubnet2 + RouteTableId: !Ref PublicRouteTable + + # ── NAT Gateway ───────────────────────────────────────────────────────────── + + NatEIP: + Type: AWS::EC2::EIP + Properties: + Domain: vpc + + NatGateway: + Type: AWS::EC2::NatGateway + Properties: + AllocationId: !GetAtt NatEIP.AllocationId + SubnetId: !Ref PublicSubnet1 + Tags: + - Key: Name + Value: agentcore-nat + + # ── Private Route Table ───────────────────────────────────────────────────── + + PrivateRouteTable: + Type: AWS::EC2::RouteTable + Properties: + VpcId: !Ref VPC + Tags: + - Key: Name + Value: agentcore-private-rt + + PrivateRoute: + Type: AWS::EC2::Route + Properties: + RouteTableId: !Ref PrivateRouteTable + DestinationCidrBlock: "0.0.0.0/0" + NatGatewayId: !Ref NatGateway + + PrivateSubnet1RTAssoc: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + SubnetId: !Ref PrivateSubnet1 + RouteTableId: !Ref PrivateRouteTable + + PrivateSubnet2RTAssoc: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + SubnetId: !Ref PrivateSubnet2 + RouteTableId: !Ref PrivateRouteTable + + # ── Security Group ────────────────────────────────────────────────────────── + + AgentCoreSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Security group for AgentCore runtimes + VpcId: !Ref VPC + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 2049 + ToPort: 2049 + CidrIp: "10.0.0.0/16" + Description: Allow NFS (S3 Files mount) from VPC + SecurityGroupEgress: + - IpProtocol: "-1" + CidrIp: "0.0.0.0/0" + Description: Allow all outbound + Tags: + - Key: Name + Value: agentcore-sg + + # ── S3 Files IAM Role ────────────────────────────────────────────────────── + + S3FilesRole: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub "s3files-${BucketName}-role" + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Service: elasticfilesystem.amazonaws.com + Action: sts:AssumeRole + Policies: + - PolicyName: s3files-bucket-access + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - s3:GetObject + - s3:PutObject + - s3:DeleteObject + - s3:ListBucket + - s3:GetBucketLocation + - s3:AbortMultipartUpload + - s3:DeleteObject* + - s3:GetObject* + - s3:List* + - s3:PutObject* + - s3:ListBucketVersions + Resource: + - !Sub "arn:aws:s3:::${BucketName}" + - !Sub "arn:aws:s3:::${BucketName}/*" + - Sid: S3ObjectPermissions + Effect: Allow + Action: + - s3:AbortMultipartUpload + - s3:DeleteObject* + - s3:GetObject* + - s3:List* + - s3:PutObject* + Resource: !Sub "arn:aws:s3:::${BucketName}/*" + - Sid: UseKmsKeyWithS3Files + Effect: Allow + Action: + - kms:GenerateDataKey + - kms:Encrypt + - kms:Decrypt + - kms:ReEncryptFrom + - kms:ReEncryptTo + Condition: + StringLike: + kms:ViaService: !Sub "s3.${AWS::Region}.amazonaws.com" + kms:EncryptionContext:aws:s3:arn: + - !Sub "arn:aws:s3:::${BucketName}" + - !Sub "arn:aws:s3:::${BucketName}/*" + Resource: !Sub "arn:aws:kms:${AWS::Region}:${AWS::AccountId}:*" + - Sid: EventBridgeManage + Effect: Allow + Action: + - events:DeleteRule + - events:DisableRule + - events:EnableRule + - events:PutRule + - events:PutTargets + - events:RemoveTargets + Condition: + StringEquals: + events:ManagedBy: elasticfilesystem.amazonaws.com + Resource: + - "arn:aws:events:*:*:rule/DO-NOT-DELETE-S3-Files*" + - Sid: EventBridgeRead + Effect: Allow + Action: + - events:DescribeRule + - events:ListRuleNamesByTarget + - events:ListRules + - events:ListTargetsByRule + Resource: + - "arn:aws:events:*:*:rule/*" + + # ── S3 Files File System ─────────────────────────────────────────────────── + + S3FilesFileSystem: + Type: AWS::S3Files::FileSystem + DependsOn: S3FilesRole + Properties: + Bucket: !Sub "arn:aws:s3:::${BucketName}" + Prefix: !Ref S3FilesPrefix + RoleArn: !GetAtt S3FilesRole.Arn + + # ── S3 Files Access Point ────────────────────────────────────────────────── + + S3FilesAccessPoint: + Type: AWS::S3Files::AccessPoint + Properties: + FileSystemId: !Ref S3FilesFileSystem + PosixUser: + Uid: 1000 + Gid: 1000 + RootDirectory: + Path: /mnt/s3files + CreationPermissions: + OwnerUid: 1000 + OwnerGid: 1000 + Permissions: "755" + + # ── S3 Files Mount Target (one per private subnet) ──────────────────────── + + S3FilesMountTarget1: + Type: AWS::S3Files::MountTarget + Properties: + FileSystemId: !Ref S3FilesFileSystem + SubnetId: !Ref PrivateSubnet1 + SecurityGroups: + - !Ref AgentCoreSecurityGroup + + S3FilesMountTarget2: + Type: AWS::S3Files::MountTarget + Properties: + FileSystemId: !Ref S3FilesFileSystem + SubnetId: !Ref PrivateSubnet2 + SecurityGroups: + - !Ref AgentCoreSecurityGroup + +Outputs: + VpcId: + Value: !Ref VPC + PrivateSubnet1Id: + Value: !Ref PrivateSubnet1 + PrivateSubnet2Id: + Value: !Ref PrivateSubnet2 + SecurityGroupId: + Value: !Ref AgentCoreSecurityGroup + S3FilesFileSystemId: + Value: !Ref S3FilesFileSystem + S3FilesAccessPointId: + Value: !Ref S3FilesAccessPoint + S3FilesAccessPointArn: + Value: !GetAtt S3FilesAccessPoint.AccessPointArn + S3FilesRoleArn: + Value: !GetAtt S3FilesRole.Arn diff --git a/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/cleanup.py b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/cleanup.py new file mode 100644 index 000000000..cc7da30ed --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/cleanup.py @@ -0,0 +1,119 @@ +""" +Clean up all resources created by setup.sh and deploy.py. + +Deletes everything except the S3 bucket: +- AgentCore Runtime endpoint and runtime +- AgentCore IAM execution role +- CloudFormation stack (VPC, S3 Files, security group, NAT, etc.) + +Usage: + python cleanup.py +""" + +import json +import os +import sys +import time + +import boto3 + + +def load_config(filename): + path = os.path.join(os.path.dirname(__file__), filename) + if not os.path.exists(path): + return {} + with open(path) as f: + if filename.endswith(".json"): + return json.load(f) + cfg = {} + for line in f: + line = line.strip() + if line and "=" in line and not line.startswith("#"): + key, value = line.split("=", 1) + cfg[key] = value.strip('"').strip("'") + return cfg + + +def main(): + runtime_cfg = load_config("runtime_config.json") + env_cfg = load_config("envvars.config") + + if not runtime_cfg: + print("Error: runtime_config.json not found.") + sys.exit(1) + + agent_name = runtime_cfg["agent_name"] + runtime_id = runtime_cfg["runtime_id"] + region = runtime_cfg["region"] + stack_name = env_cfg.get("AGENTCORE_STACK_NAME", "agentcore-codex") + session = boto3.Session(region_name=region) + account_id = session.client("sts").get_caller_identity()["Account"] + control = session.client("bedrock-agentcore-control", region_name=region) + iam = session.client("iam") + cfn = session.client("cloudformation") + + print(f"Cleaning up resources for: {agent_name}\n") + + # 1. Delete AgentCore endpoints + try: + endpoints = control.list_agent_runtime_endpoints(agentRuntimeId=runtime_id) + for ep in endpoints.get("runtimeEndpoints", []): + name = ep["name"] + if name == "DEFAULT": + continue + print(f" Deleting endpoint: {name}") + control.delete_agent_runtime_endpoint( + agentRuntimeId=runtime_id, endpointName=name + ) + if endpoints.get("runtimeEndpoints"): + print(" Waiting for endpoint deletion...") + time.sleep(30) + except Exception as e: + print(f" Warning: {e}") + + # 2. Delete AgentCore runtime + try: + print(f" Deleting runtime: {runtime_id}") + control.delete_agent_runtime(agentRuntimeId=runtime_id) + print(" Waiting for runtime deletion...") + time.sleep(30) + except Exception as e: + print(f" Warning: {e}") + + # 3. Delete AgentCore IAM execution role + role_name = f"agentcore-{agent_name}-role" + try: + policies = iam.list_role_policies(RoleName=role_name) + for policy_name in policies.get("PolicyNames", []): + iam.delete_role_policy(RoleName=role_name, PolicyName=policy_name) + iam.delete_role(RoleName=role_name) + print(f" Deleted IAM role: {role_name}") + except iam.exceptions.NoSuchEntityException: + print(f" IAM role not found: {role_name}") + except Exception as e: + print(f" Warning: {e}") + + # 4. Delete CloudFormation stack (VPC, S3 Files, SG, NAT, etc.) + try: + print(f" Deleting CloudFormation stack: {stack_name}") + cfn.delete_stack(StackName=stack_name) + print(" Waiting for stack deletion (this may take a few minutes)...") + waiter = cfn.get_waiter("stack_delete_complete") + waiter.wait(StackName=stack_name, WaiterConfig={"Delay": 15, "MaxAttempts": 40}) + print(f" Stack deleted: {stack_name}") + except Exception as e: + print(f" Warning: {e}") + + # 5. Remove local config files + for f in ["runtime_config.json", "envvars.config"]: + path = os.path.join(os.path.dirname(__file__), f) + if os.path.exists(path): + os.remove(path) + + bucket_name = f"agentcore-{account_id}" + print(f"\nCleanup complete for {agent_name}") + print(f" (bucket s3://{bucket_name} was kept)") + + +if __name__ == "__main__": + main() diff --git a/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/cleanup.sh b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/cleanup.sh new file mode 100755 index 000000000..d803197ec --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/cleanup.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$SCRIPT_DIR" + +echo "============================================================" +echo " AgentCore Cleanup" +echo "============================================================" +echo "" + +python3 cleanup.py + +echo "" +echo "Done." diff --git a/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/codex-config.toml b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/codex-config.toml new file mode 100644 index 000000000..ebf442cc7 --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/codex-config.toml @@ -0,0 +1,2 @@ +model_provider = "openai" +model = "gpt-4o" diff --git a/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/deploy.py b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/deploy.py new file mode 100644 index 000000000..9a3ec4c77 --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/deploy.py @@ -0,0 +1,364 @@ +""" +Deploy Codex agent to AgentCore Runtime via OpenAPI API key using a container image from ECR. + +Run setup.sh first, then: + python deploy.py + +Reads configuration from envvars.config (created by setup.sh). +""" + +import json +import os +import sys +import time + +import boto3 + +# ── Load config ────────────────────────────────────────────────────────────── + +def load_dotconfig(): + config_path = os.path.join(os.path.dirname(__file__), "envvars.config") + cfg = {} + if os.path.exists(config_path): + with open(config_path) as f: + for line in f: + line = line.strip() + if line and "=" in line and not line.startswith("#"): + key, value = line.split("=", 1) + cfg[key] = value + return cfg + +file_cfg = load_dotconfig() + +def cfg(key, default=None): + return file_cfg.get(key) or os.environ.get(key) or default + +# ── Configuration ──────────────────────────────────────────────────────────── + +REGION = cfg("AGENTCORE_REGION", boto3.session.Session().region_name or "us-west-2") + +session = boto3.Session(region_name=REGION) +ACCOUNT_ID = session.client("sts").get_caller_identity()["Account"] + +AGENT_NAME = cfg("AGENTCORE_AGENT_NAME", f"codex_{int(time.time()) % 100000}") +ECR_URI = cfg("AGENTCORE_ECR_URI") +SUBNET_1 = cfg("AGENTCORE_SUBNET_1") +SUBNET_2 = cfg("AGENTCORE_SUBNET_2") +SECURITY_GROUP = cfg("AGENTCORE_SECURITY_GROUP") +S3FILES_AP_ARN = cfg("AGENTCORE_S3FILES_AP_ARN") +S3FILES_BUCKET = cfg("AGENTCORE_S3FILES_BUCKET", f"agentcore-{ACCOUNT_ID}") +OPENAI_SECRET_NAME = "openai/codex" +OPENAI_SECRET_ARN = f"arn:aws:secretsmanager:{REGION}:{ACCOUNT_ID}:secret:{OPENAI_SECRET_NAME}" + +if not ECR_URI: + print("Error: AGENTCORE_ECR_URI not found. Run setup.sh first.") + sys.exit(1) + +if not all([SUBNET_1, SUBNET_2, SECURITY_GROUP]): + print("Error: VPC config (subnets, security group) not found. Run setup.sh first.") + sys.exit(1) + +PROTOCOL = "HTTP" +S3FILES_MOUNT_PATH = "/mnt/s3files" + +print(f"Region: {REGION}") +print(f"Account: {ACCOUNT_ID}") +print(f"Agent: {AGENT_NAME}") +print(f"Image: {ECR_URI}") +print(f"Subnets: {SUBNET_1}, {SUBNET_2}") +print(f"SG: {SECURITY_GROUP}") +if S3FILES_AP_ARN: + print(f"S3 Files: {S3FILES_AP_ARN}") + print(f"Mount: {S3FILES_MOUNT_PATH}") + + +# ── Step 1: Create IAM Execution Role ──────────────────────────────────────── + +def create_execution_role() -> str: + iam = session.client("iam") + role_name = f"agentcore-{AGENT_NAME}-role" + + trust_policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "bedrock-agentcore.amazonaws.com"}, + "Action": "sts:AssumeRole", + "Condition": {"StringEquals": {"aws:SourceAccount": ACCOUNT_ID}}, + }, + { + "Sid": "AllowS3FilesAssumeRole", + "Effect": "Allow", + "Principal": {"Service": "elasticfilesystem.amazonaws.com"}, + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": {"aws:SourceAccount": ACCOUNT_ID}, + "ArnLike": {"aws:SourceArn": f"arn:aws:s3files:{REGION}:{ACCOUNT_ID}:file-system/*"}, + }, + }, + ], + } + + inline_policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": ["logs:DescribeLogStreams", "logs:CreateLogGroup"], + "Resource": [f"arn:aws:logs:{REGION}:{ACCOUNT_ID}:log-group:/aws/bedrock-agentcore/runtimes/*"], + }, + { + "Effect": "Allow", + "Action": ["logs:DescribeLogGroups"], + "Resource": [f"arn:aws:logs:{REGION}:{ACCOUNT_ID}:log-group:*"], + }, + { + "Effect": "Allow", + "Action": ["logs:CreateLogStream", "logs:PutLogEvents"], + "Resource": [f"arn:aws:logs:{REGION}:{ACCOUNT_ID}:log-group:/aws/bedrock-agentcore/runtimes/*:log-stream:*"], + }, + { + "Effect": "Allow", + "Action": ["xray:PutTraceSegments", "xray:PutTelemetryRecords", "xray:GetSamplingRules", "xray:GetSamplingTargets"], + "Resource": ["*"], + }, + { + "Effect": "Allow", + "Action": "cloudwatch:PutMetricData", + "Resource": "*", + "Condition": {"StringEquals": {"cloudwatch:namespace": "bedrock-agentcore"}}, + }, + { + "Sid": "SecretsManager", + "Effect": "Allow", + "Action": ["secretsmanager:GetSecretValue"], + "Resource": f"{OPENAI_SECRET_ARN}*", + }, + { + "Sid": "ECRPull", + "Effect": "Allow", + "Action": ["ecr:GetAuthorizationToken"], + "Resource": ["*"], + }, + { + "Sid": "ECRImage", + "Effect": "Allow", + "Action": ["ecr:BatchGetImage", "ecr:GetDownloadUrlForLayer"], + "Resource": [f"arn:aws:ecr:{REGION}:{ACCOUNT_ID}:repository/agentcore-codex"], + }, + { + "Sid": "S3Files", + "Effect": "Allow", + "Action": [ + "s3files:GetAccessPoint", + "s3files:GetFileSystem", + "s3files:GetMountTarget", + "s3files:DescribeMountTargets", + "s3files:ListMountTargets", + "s3files:ClientMount", + "s3files:ClientWrite", + "s3files:ClientRootAccess", + ], + "Resource": [ + S3FILES_AP_ARN, + S3FILES_AP_ARN.rsplit("/access-point/", 1)[0], + ], + }, + { + "Sid": "EFSClientAccess", + "Effect": "Allow", + "Action": [ + "elasticfilesystem:ClientMount", + "elasticfilesystem:ClientWrite", + ], + "Resource": f"arn:aws:elasticfilesystem:{REGION}:{ACCOUNT_ID}:file-system/*", + "Condition": { + "ArnLike": { + "elasticfilesystem:AccessPointArn": f"arn:aws:elasticfilesystem:{REGION}:{ACCOUNT_ID}:access-point/*", + } + }, + }, + { + "Sid": "EFSDescribe", + "Effect": "Allow", + "Action": [ + "elasticfilesystem:DescribeAccessPoints", + "elasticfilesystem:DescribeMountTargets", + ], + "Resource": [ + f"arn:aws:elasticfilesystem:{REGION}:{ACCOUNT_ID}:file-system/*", + f"arn:aws:elasticfilesystem:{REGION}:{ACCOUNT_ID}:access-point/*", + ], + }, + { + "Sid": "S3BucketPermissions", + "Effect": "Allow", + "Action": [ + "s3:ListBucket", + "s3:ListBucketVersions", + ], + "Resource": [f"arn:aws:s3:::{S3FILES_BUCKET}"], + "Condition": {"StringEquals": {"aws:ResourceAccount": ACCOUNT_ID}}, + }, + { + "Sid": "S3ObjectPermissions", + "Effect": "Allow", + "Action": [ + "s3:AbortMultipartUpload", + "s3:DeleteObject*", + "s3:GetObject*", + "s3:List*", + "s3:PutObject*", + ], + "Resource": [f"arn:aws:s3:::{S3FILES_BUCKET}/*"], + "Condition": {"StringEquals": {"aws:ResourceAccount": ACCOUNT_ID}}, + }, + { + "Sid": "EventBridgeManage", + "Effect": "Allow", + "Action": [ + "events:DeleteRule", + "events:DisableRule", + "events:EnableRule", + "events:PutRule", + "events:PutTargets", + "events:RemoveTargets", + ], + "Resource": ["arn:aws:events:*:*:rule/DO-NOT-DELETE-S3-Files*"], + "Condition": {"StringEquals": {"events:ManagedBy": "elasticfilesystem.amazonaws.com"}}, + }, + { + "Sid": "EventBridgeRead", + "Effect": "Allow", + "Action": [ + "events:DescribeRule", + "events:ListRuleNamesByTarget", + "events:ListRules", + "events:ListTargetsByRule", + ], + "Resource": ["arn:aws:events:*:*:rule/*"], + }, + ], + } + + try: + resp = iam.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=json.dumps(trust_policy), + Description=f"Execution role for {AGENT_NAME}", + ) + role_arn = resp["Role"]["Arn"] + print(f"\nCreated IAM role: {role_arn}") + except iam.exceptions.EntityAlreadyExistsException: + role_arn = f"arn:aws:iam::{ACCOUNT_ID}:role/{role_name}" + print(f"\nIAM role exists: {role_arn}") + + iam.put_role_policy( + RoleName=role_name, + PolicyName=f"{AGENT_NAME}-policy", + PolicyDocument=json.dumps(inline_policy), + ) + + print("Waiting 10s for IAM propagation...") + time.sleep(10) + return role_arn + + +# ── Step 2: Create AgentCore Runtime ───────────────────────────────────────── + +def create_runtime(role_arn: str) -> dict: + control = session.client("bedrock-agentcore-control", region_name=REGION) + + create_params = dict( + agentRuntimeName=AGENT_NAME, + agentRuntimeArtifact={ + "containerConfiguration": { + "containerUri": ECR_URI, + } + }, + roleArn=role_arn, + networkConfiguration={ + "networkMode": "VPC", + "networkModeConfig": { + "subnets": [SUBNET_1, SUBNET_2], + "securityGroups": [SECURITY_GROUP], + }, + }, + protocolConfiguration={"serverProtocol": PROTOCOL}, + environmentVariables={ + "AWS_REGION": REGION, + }, + description="Codex agent on AgentCore Runtime with S3 Files, backed by OpenAI gpt-4o", + ) + + if S3FILES_AP_ARN: + create_params["filesystemConfigurations"] = [ + { + "s3FilesAccessPoint": { + "accessPointArn": S3FILES_AP_ARN, + "mountPath": S3FILES_MOUNT_PATH, + } + } + ] + + print(f"\nCreating AgentCore Runtime '{AGENT_NAME}'...") + response = control.create_agent_runtime(**create_params) + + runtime_id = response["agentRuntimeId"] + runtime_arn = response["agentRuntimeArn"] + print(f"Runtime created: {runtime_id}") + + print("Waiting for runtime to be ready...") + while True: + status_resp = control.get_agent_runtime(agentRuntimeId=runtime_id) + status = status_resp["status"] + print(f" Status: {status}") + if status == "READY": + break + if status in ("CREATE_FAILED", "UPDATE_FAILED"): + print(f"Failed: {status_resp.get('failureReason', 'Unknown')}") + sys.exit(1) + time.sleep(15) + + return {"runtime_id": runtime_id, "runtime_arn": runtime_arn} + + +# ── Main ───────────────────────────────────────────────────────────────────── + +def main(): + print("=" * 60) + print(f"Deploying {AGENT_NAME} to AgentCore Runtime") + print(" (VPC mode + Codex + Bedrock Claude Sonnet 4.6 + S3 Files)") + print("=" * 60) + + role_arn = create_execution_role() + runtime = create_runtime(role_arn) + + config = { + "agent_name": AGENT_NAME, + "runtime_id": runtime["runtime_id"], + "runtime_arn": runtime["runtime_arn"], + "region": REGION, + "ecr_uri": ECR_URI, + } + if S3FILES_AP_ARN: + config["s3files_access_point_arn"] = S3FILES_AP_ARN + config["s3files_mount_path"] = S3FILES_MOUNT_PATH + + config_path = os.path.join(os.path.dirname(__file__), "runtime_config.json") + with open(config_path, "w") as f: + json.dump(config, f, indent=2) + + print("\n" + "=" * 60) + print("Deployment complete!") + print(f" Runtime ARN: {runtime['runtime_arn']}") + if S3FILES_AP_ARN: + print(f" S3 Files mounted at: {S3FILES_MOUNT_PATH}") + print(" Config saved to: runtime_config.json") + print("\n Test with: python invoke.py") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/entrypoint.sh b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/entrypoint.sh new file mode 100644 index 000000000..2a4e26e48 --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/entrypoint.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "Fetching OpenAI credentials from Secrets Manager..." + +SECRET=$(aws secretsmanager get-secret-value \ + --secret-id "openai/codex" \ + --region "${AWS_REGION:-us-west-2}" \ + --query SecretString \ + --output text) + +export CODEX_API_KEY=$(echo "$SECRET" | jq -r .api_key) + +echo "Credentials loaded. Starting server..." +exec node /app/server.js diff --git a/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/exec_cmd.py b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/exec_cmd.py new file mode 100644 index 000000000..fece1cbf2 --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/exec_cmd.py @@ -0,0 +1,112 @@ +""" +Execute a shell command on a running AgentCore Runtime session and stream output. + +Usage: + python exec_cmd.py --session "ls -la /mnt/s3files" +""" + +import json +import os +import sys + +import boto3 +from botocore.config import Config + +# ── Load config ────────────────────────────────────────────────────────────── + +def load_dotconfig(): + config_path = os.path.join(os.path.dirname(__file__), "envvars.config") + cfg = {} + if os.path.exists(config_path): + with open(config_path) as f: + for line in f: + line = line.strip() + if line and "=" in line and not line.startswith("#"): + key, value = line.split("=", 1) + cfg[key] = value.strip('"').strip("'") + return cfg + +file_cfg = load_dotconfig() + +def cfg(key, default=None): + return file_cfg.get(key) or os.environ.get(key) or default + +# ── Configuration ──────────────────────────────────────────────────────────── + +REGION = cfg("AGENTCORE_REGION", "us-west-2") + + +def load_config() -> dict: + config_path = os.path.join(os.path.dirname(__file__), "runtime_config.json") + try: + with open(config_path) as f: + return json.load(f) + except FileNotFoundError: + print("Error: runtime_config.json not found. Run deploy.py first.") + sys.exit(1) + + +def exec_command(runtime_arn: str, session_id: str, command: str): + client = boto3.client( + "bedrock-agentcore", + region_name=REGION, + config=Config(read_timeout=900), + ) + + body = {"command": command} + + response = client.invoke_agent_runtime_command( + agentRuntimeArn=runtime_arn, + runtimeSessionId=session_id, + body=body, + ) + + request_id = response.get("ResponseMetadata", {}).get("RequestId", "N/A") + + print(f"Session: {response.get('runtimeSessionId', 'N/A')}") + print(f"Request ID: {request_id}") + print(f"Status: {response.get('statusCode', 'N/A')}") + print() + + for event in response["stream"]: + chunk = event.get("chunk", {}) + if "contentDelta" in chunk: + delta = chunk["contentDelta"] + print(delta.get("stdout", ""), end="", flush=True) + print(delta.get("stderr", ""), end="", flush=True) + + +def main(): + config = load_config() + runtime_arn = config["runtime_arn"] + + args = sys.argv[1:] + session_id = None + + if "--session" in args: + idx = args.index("--session") + session_id = args[idx + 1] + args = args[:idx] + args[idx + 2:] + + if not session_id: + session_id = os.environ.get("SESSION_ID") + + if not session_id: + print("Error: session ID required. Use --session or set SESSION_ID env var.") + sys.exit(1) + + if not args: + print("Usage: python exec_cmd.py --session ''") + sys.exit(1) + + command = " ".join(args) + + print(f"Runtime: {runtime_arn}") + print(f"Command: {command}") + print() + + exec_command(runtime_arn, session_id, command) + + +if __name__ == "__main__": + main() diff --git a/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/invoke.py b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/invoke.py new file mode 100644 index 000000000..7542ee4c4 --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/invoke.py @@ -0,0 +1,130 @@ +""" +Invoke the Claude Code agent deployed on AgentCore Runtime. + +Usage: + python invoke.py + python invoke.py "Write a Python function that sorts a list" + python invoke.py --session "Now add type hints to it" +""" + +import json +import os +import sys +import uuid + +import boto3 +from botocore.config import Config +from botocore.exceptions import ClientError + +# ── Load config ────────────────────────────────────────────────────────────── + +def load_dotconfig(): + config_path = os.path.join(os.path.dirname(__file__), "envvars.config") + cfg = {} + if os.path.exists(config_path): + with open(config_path) as f: + for line in f: + line = line.strip() + if line and "=" in line and not line.startswith("#"): + key, value = line.split("=", 1) + cfg[key] = value.strip('"').strip("'") + return cfg + +file_cfg = load_dotconfig() + +def cfg(key, default=None): + return file_cfg.get(key) or os.environ.get(key) or default + +# ── Configuration ──────────────────────────────────────────────────────────── + +REGION = cfg("AGENTCORE_REGION", "us-west-2") + + +def load_config() -> dict: + config_path = os.path.join(os.path.dirname(__file__), "runtime_config.json") + try: + with open(config_path) as f: + return json.load(f) + except FileNotFoundError: + print("Error: runtime_config.json not found. Run deploy.py first.") + sys.exit(1) + + +def invoke(runtime_arn: str, prompt: str, region: str, session_id: str = None) -> dict: + client = boto3.client( + "bedrock-agentcore", + region_name=region, + config=Config(read_timeout=900), + ) + + if not session_id: + session_id = str(uuid.uuid4()) + + payload_data = {"prompt": prompt} + + try: + response = client.invoke_agent_runtime( + agentRuntimeArn=runtime_arn, + runtimeSessionId=session_id, + payload=json.dumps(payload_data).encode("utf-8"), + ) + except ClientError as exc: + request_id = exc.response.get("ResponseMetadata", {}).get("RequestId", "N/A") + print(f" Session ID: {session_id}") + print(f" Request ID: {request_id}") + print(f" Error: {exc}") + return {"_runtimeSessionId": session_id} + + body = json.loads(response["response"].read().decode("utf-8")) + runtime_session = response.get("runtimeSessionId", session_id) + request_id = response.get("ResponseMetadata", {}).get("RequestId", "N/A") + + print(f" Session ID: {runtime_session}") + print(f" Request ID: {request_id}") + print(f" Status: {response.get('statusCode', 'N/A')}") + + body["_runtimeSessionId"] = runtime_session + return body + + +def main(): + config = load_config() + runtime_arn = config["runtime_arn"] + region = config["region"] + + args = sys.argv[1:] + session_id = None + + if "--session" in args: + idx = args.index("--session") + session_id = args[idx + 1] + args = args[:idx] + args[idx + 2:] + + if args: + prompts = [" ".join(args)] + else: + prompts = [ + "What is 2 + 2?", + "Now multiply that result by 10.", + ] + + print(f"Invoking agent: {runtime_arn}") + if session_id: + print(f"Resuming session: {session_id}") + print() + + for prompt in prompts: + print(f"--- Prompt: {prompt}") + result = invoke(runtime_arn, prompt, region, session_id) + print(f"--- Response:\n{result.get('response', result)}") + session_id = result.get("_runtimeSessionId", session_id) + print(f"--- Session ID: {session_id}") + print() + + if session_id: + print("To continue this conversation:") + print(f" python invoke.py --session {session_id} \"your next prompt\"") + + +if __name__ == "__main__": + main() diff --git a/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/server.js b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/server.js new file mode 100644 index 000000000..ff28a9a1b --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/server.js @@ -0,0 +1,88 @@ +const http = require("http"); +const { spawn } = require("child_process"); + +const PORT = process.env.PORT || 8080; + +function runCodex(prompt, sessionId) { + return new Promise((resolve, reject) => { + let args; + if (sessionId) { + args = ["exec", "resume", sessionId, prompt, "--json", "--sandbox", "danger-full-access", "--skip-git-repo-check"]; + } else { + args = ["exec", prompt, "--json", "--sandbox", "danger-full-access", "--skip-git-repo-check"]; + } + + console.log(`[runCodex] sessionId=${sessionId || "(none)"} prompt="${prompt}"`); + console.log(`[runCodex] args: ${JSON.stringify(args)}`); + + const proc = spawn("codex", args, { + env: { ...process.env, HOME: "/home/agent" }, + cwd: "/home/agent", + timeout: 300_000, + stdio: ["ignore", "pipe", "pipe"], + }); + + let stdout = ""; + let stderr = ""; + + proc.stdout.on("data", (d) => (stdout += d)); + proc.stderr.on("data", (d) => (stderr += d)); + + proc.on("close", (code) => { + console.log(`[runCodex] exited code=${code} stderr="${stderr}" stdout="${stdout.slice(0, 200)}"`); + if (code !== 0) { + reject(new Error(`codex exited ${code}: ${stderr}`)); + return; + } + // --json emits one JSON object per line, last one has the result + const lines = stdout.trim().split("\n").filter(Boolean); + try { + const last = JSON.parse(lines[lines.length - 1]); + resolve({ + response: last.result || last.message || stdout.trim(), + sessionId: last.session_id || null, + }); + } catch { + resolve({ response: stdout.trim(), sessionId: null }); + } + }); + proc.on("error", reject); + }); +} + +function readBody(req) { + return new Promise((resolve) => { + let data = ""; + req.on("data", (chunk) => (data += chunk)); + req.on("end", () => resolve(data)); + }); +} + +const server = http.createServer(async (req, res) => { + if (req.method === "GET") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ status: "healthy" })); + return; + } + + if (req.method === "POST") { + try { + const body = await readBody(req); + const { prompt, sessionId } = JSON.parse(body); + const result = await runCodex(prompt, sessionId); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify(result)); + } catch (err) { + res.writeHead(500, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: err.message })); + } + return; + } + + res.writeHead(405); + res.end(); +}); + +server.listen(PORT, () => { + console.log(`Codex agent listening on port ${PORT}`); +}); diff --git a/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/setup.sh b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/setup.sh new file mode 100755 index 000000000..1019e1392 --- /dev/null +++ b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/setup.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +set -euo pipefail + +REGION="${1:-us-west-2}" +STACK_NAME="agentcore-codex-demo" + +ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) +BUCKET_NAME="agentcore-${ACCOUNT_ID}" +AGENT_NAME="codex_$(date +%s | tail -c 6)" +ECR_REPO="agentcore-codex" +IMAGE_TAG="${AGENT_NAME}" +ECR_URI="${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${ECR_REPO}:${IMAGE_TAG}" + +echo "Region: ${REGION}" +echo "Account: ${ACCOUNT_ID}" +echo "Bucket: ${BUCKET_NAME}" +echo "Agent: ${AGENT_NAME}" +echo "Stack: ${STACK_NAME}" + +# ── Create S3 bucket (skip if it already exists) ───────────────────────────── + +if aws s3api head-bucket --bucket "${BUCKET_NAME}" 2>/dev/null; then + echo "Bucket already exists: ${BUCKET_NAME}" +else + echo "Creating bucket: ${BUCKET_NAME}" + if [ "${REGION}" = "us-east-1" ]; then + aws s3api create-bucket --bucket "${BUCKET_NAME}" --region "${REGION}" + else + aws s3api create-bucket \ + --bucket "${BUCKET_NAME}" \ + --region "${REGION}" \ + --create-bucket-configuration LocationConstraint="${REGION}" + fi + echo "Bucket created: ${BUCKET_NAME}" +fi + +aws s3api put-bucket-versioning \ + --bucket "${BUCKET_NAME}" \ + --versioning-configuration Status=Enabled \ + --region "${REGION}" +echo "Bucket versioning enabled" + +# ── Deploy CloudFormation stack ────────────────────────────────────────────── + +echo "" +echo "Deploying CloudFormation stack: ${STACK_NAME}..." +aws cloudformation deploy \ + --template-file cfn-vpc.yaml \ + --stack-name "${STACK_NAME}" \ + --region "${REGION}" \ + --parameter-overrides BucketName="${BUCKET_NAME}" \ + --capabilities CAPABILITY_NAMED_IAM \ + --no-fail-on-empty-changeset + +echo "Reading stack outputs..." +CFN_OUTPUTS=$(aws cloudformation describe-stacks \ + --stack-name "${STACK_NAME}" \ + --region "${REGION}" \ + --query "Stacks[0].Outputs" \ + --output json) + +get_output() { + echo "${CFN_OUTPUTS}" | python3 -c " +import json, sys +outputs = json.load(sys.stdin) +for o in outputs: + if o['OutputKey'] == '$1': + print(o['OutputValue']) + break +" +} + +VPC_ID=$(get_output VpcId) +PRIVATE_SUBNET_1=$(get_output PrivateSubnet1Id) +PRIVATE_SUBNET_2=$(get_output PrivateSubnet2Id) +SECURITY_GROUP_ID=$(get_output SecurityGroupId) +S3FILES_FS_ID=$(get_output S3FilesFileSystemId) +S3FILES_AP_ID=$(get_output S3FilesAccessPointId) +S3FILES_AP_ARN=$(get_output S3FilesAccessPointArn) + +echo " VPC: ${VPC_ID}" +echo " Private Subnet1: ${PRIVATE_SUBNET_1}" +echo " Private Subnet2: ${PRIVATE_SUBNET_2}" +echo " Security Group: ${SECURITY_GROUP_ID}" +echo " S3 Files FS: ${S3FILES_FS_ID}" +echo " S3 Files AP: ${S3FILES_AP_ID}" + +# ── Create ECR repository (skip if it already exists) ──────────────────────── + +if aws ecr describe-repositories --repository-names "${ECR_REPO}" --region "${REGION}" >/dev/null 2>&1; then + echo "ECR repo already exists: ${ECR_REPO}" +else + echo "Creating ECR repo: ${ECR_REPO}" + aws ecr create-repository --repository-name "${ECR_REPO}" --region "${REGION}" + echo "ECR repo created" +fi + +# ── Build arm64 Docker image and push to ECR ───────────────────────────────── + +echo "Logging into ECR..." +aws ecr get-login-password --region "${REGION}" | \ + docker login --username AWS --password-stdin "${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com" + +echo "Building arm64 Docker image..." +docker buildx build \ + --platform linux/arm64 \ + -t "${ECR_URI}" \ + -f Dockerfile \ + --push \ + . + +echo "Image pushed: ${ECR_URI}" + +# ── Save config ────────────────────────────────────────────────────────────── + +cat > envvars.config < dict: + config_path = os.path.join(os.path.dirname(__file__), "runtime_config.json") + try: + with open(config_path) as f: + return json.load(f) + except FileNotFoundError: + print("Error: runtime_config.json not found. Run deploy.py first.") + sys.exit(1) + + +def update_runtime(runtime_id: str, role_arn: str) -> dict: + control = session.client("bedrock-agentcore-control", region_name=REGION) + + update_params = dict( + agentRuntimeId=runtime_id, + agentRuntimeArtifact={ + "containerConfiguration": { + "containerUri": ECR_URI, + } + }, + roleArn=role_arn, + networkConfiguration={ + "networkMode": "VPC", + "networkModeConfig": { + "subnets": [SUBNET_1, SUBNET_2], + "securityGroups": [SECURITY_GROUP], + }, + }, + protocolConfiguration={"serverProtocol": PROTOCOL}, + description="Claude Code agent on AgentCore Runtime with S3 Files", + ) + + if S3FILES_AP_ARN: + update_params["filesystemConfigurations"] = [ + { + "s3FilesAccessPoint": { + "accessPointArn": S3FILES_AP_ARN, + "mountPath": S3FILES_MOUNT_PATH, + } + } + ] + + print(f"\nUpdating AgentCore Runtime '{runtime_id}'...") + response = control.update_agent_runtime(**update_params) + + runtime_arn = response["agentRuntimeArn"] + print(f"Update initiated: {runtime_id}") + + print("Waiting for runtime to be ready...") + while True: + status_resp = control.get_agent_runtime(agentRuntimeId=runtime_id) + status = status_resp["status"] + print(f" Status: {status}") + if status == "READY": + break + if status in ("CREATE_FAILED", "UPDATE_FAILED"): + print(f"Failed: {status_resp.get('failureReason', 'Unknown')}") + sys.exit(1) + time.sleep(15) + + return {"runtime_id": runtime_id, "runtime_arn": runtime_arn} + + +def main(): + existing = load_runtime_config() + runtime_id = existing["runtime_id"] + role_arn = f"arn:aws:iam::{ACCOUNT_ID}:role/agentcore-{existing['agent_name']}-role" + + print("=" * 60) + print(f"Updating runtime: {runtime_id}") + print(f" Image: {ECR_URI}") + print(f" Role: {role_arn}") + if S3FILES_AP_ARN: + print(f" S3 Files: {S3FILES_AP_ARN}") + print(f" Mount: {S3FILES_MOUNT_PATH}") + print("=" * 60) + + runtime = update_runtime(runtime_id, role_arn) + + existing.update({ + "runtime_id": runtime["runtime_id"], + "runtime_arn": runtime["runtime_arn"], + "ecr_uri": ECR_URI, + }) + if S3FILES_AP_ARN: + existing["s3files_access_point_arn"] = S3FILES_AP_ARN + existing["s3files_mount_path"] = S3FILES_MOUNT_PATH + + config_path = os.path.join(os.path.dirname(__file__), "runtime_config.json") + with open(config_path, "w") as f: + json.dump(existing, f, indent=2) + + print("\n" + "=" * 60) + print("Update complete!") + print(f" Runtime ARN: {runtime['runtime_arn']}") + print(" Config saved to: runtime_config.json") + print("=" * 60) + + +if __name__ == "__main__": + main() From 677c123a59b5edd2010409a8c0efab91445a7baa Mon Sep 17 00:00:00 2001 From: EC2 Default User Date: Tue, 19 May 2026 05:01:07 +0000 Subject: [PATCH 2/4] Add Sushant20 to CONTRIBUTORS.md --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 1460dc751..faa22e96a 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -40,6 +40,7 @@ - Shanicus Yee - sssumarss - sundargthb +- Sushant20 - vedashree1110 - vedashreevinay - Venkatakrishna Pullela From 98eafd7b9daa2849232d773a321717c875064c56 Mon Sep 17 00:00:00 2001 From: EC2 Default User Date: Tue, 19 May 2026 05:10:57 +0000 Subject: [PATCH 3/4] Small improvements and removed claude code reference --- .../README.md | 80 ++++++++++++++----- .../cfn-vpc.yaml | 2 +- .../deploy.py | 2 +- .../entrypoint.sh | 17 +++- .../invoke.py | 2 +- .../update.py | 2 +- 6 files changed, 77 insertions(+), 28 deletions(-) diff --git a/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/README.md b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/README.md index 04a472c26..8e6d56c6c 100644 --- a/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/README.md +++ b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/README.md @@ -11,28 +11,29 @@ Deploys Codex CLI as an HTTP agent on AWS Bedrock AgentCore Runtime, with an S3 │ (Codex CLI) │ │ (Codex CLI) │ │ │ │ │ │ /mnt/s3files ──────────┼────┐ │ /mnt/s3files ──────────┼────┐ - └─────────────────────────┘ │ └─────────────────────────┘ │ - │ │ - ▼ ▼ - ┌──────────────────────────────────────────────────┐ - │ S3 Files File System │ - │ │ - │ ┌────────────────────────┐ │ - │ │ S3 Files Access Point │ │ - │ │ (uid/gid 1000) │ │ - │ └───────────┬────────────┘ │ - └──────────────┼───────────────────────────────────┘ - │ - ▼ - ┌──────────────────────────────┐ - │ S3 Bucket │ - │ (agentcore-) │ - │ │ - │ agents/ │ - │ ├── skills/ │ - │ ├── results/ │ - │ └── ... │ - └──────────────────────────────┘ + └──────────┬──────────────┘ │ └──────────┬──────────────┘ │ + │ │ │ │ + │ Fetch API key │ │ Fetch API key │ + ▼ ▼ ▼ ▼ + ┌──────────────────────┐ ┌────────────────────────────────────────┐ + │ AWS Secrets Manager │ │ S3 Files File System │ + │ openai/codex │ │ │ + │ {"api_key":"sk-…"} │ │ ┌────────────────────────┐ │ + └──────────────────────┘ │ │ S3 Files Access Point │ │ + │ │ (uid/gid 1000) │ │ + │ └───────────┬────────────┘ │ + └──────────────┼─────────────────────────┘ + │ + ▼ + ┌──────────────────────────────┐ + │ S3 Bucket │ + │ (agentcore-) │ + │ │ + │ agents/ │ + │ ├── skills/ │ + │ ├── results/ │ + │ └── ... │ + └──────────────────────────────┘ ``` Multiple runtime sessions mount the same S3 Files file system, enabling agents to share skills, results, and data across independent invocations. @@ -80,6 +81,41 @@ docker buildx create --use The CloudFormation stack creates an IAM role (`S3FilesRole`) with the permissions required by S3 Files (S3, KMS, and EventBridge). For the full list of required policies, see the [S3 Files prerequisite policies](https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-prereq-policies.html) documentation. +### OpenAI API Key Setup + +**IMPORTANT**: Before deploying, you must create an AWS Secrets Manager secret containing your OpenAI API key. The container will fetch this secret at startup. + +1. Obtain an OpenAI API key from [platform.openai.com/api-keys](https://platform.openai.com/api-keys) + +2. Create the secret in AWS Secrets Manager: + +```bash +aws secretsmanager create-secret \ + --name "openai/codex" \ + --description "OpenAI API key for Codex CLI" \ + --secret-string '{"api_key":"sk-YOUR-OPENAI-KEY-HERE"}' \ + --region us-west-2 +``` + +Replace `sk-YOUR-OPENAI-KEY-HERE` with your actual OpenAI API key, and adjust the region to match your deployment region. + +To update an existing secret: + +```bash +aws secretsmanager update-secret \ + --secret-id "openai/codex" \ + --secret-string '{"api_key":"sk-YOUR-NEW-KEY-HERE"}' \ + --region us-west-2 +``` + +To verify the secret was created: + +```bash +aws secretsmanager describe-secret \ + --secret-id "openai/codex" \ + --region us-west-2 +``` + ## Step-by-step guide ### Step 1 — Infrastructure setup (CloudFormation) diff --git a/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/cfn-vpc.yaml b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/cfn-vpc.yaml index af973dde2..8a7d3673c 100644 --- a/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/cfn-vpc.yaml +++ b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/cfn-vpc.yaml @@ -1,5 +1,5 @@ AWSTemplateFormatVersion: "2010-09-09" -Description: VPC infrastructure for AgentCore Claude Code with S3 Files +Description: VPC infrastructure for AgentCore Codex CLI with S3 Files Parameters: VpcCidr: diff --git a/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/deploy.py b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/deploy.py index 9a3ec4c77..b3f64ec98 100644 --- a/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/deploy.py +++ b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/deploy.py @@ -329,7 +329,7 @@ def create_runtime(role_arn: str) -> dict: def main(): print("=" * 60) print(f"Deploying {AGENT_NAME} to AgentCore Runtime") - print(" (VPC mode + Codex + Bedrock Claude Sonnet 4.6 + S3 Files)") + print(" (VPC mode + Codex CLI + OpenAI gpt-4o + S3 Files)") print("=" * 60) role_arn = create_execution_role() diff --git a/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/entrypoint.sh b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/entrypoint.sh index 2a4e26e48..d82bbf8f4 100644 --- a/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/entrypoint.sh +++ b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/entrypoint.sh @@ -3,13 +3,26 @@ set -euo pipefail echo "Fetching OpenAI credentials from Secrets Manager..." -SECRET=$(aws secretsmanager get-secret-value \ +if ! SECRET=$(aws secretsmanager get-secret-value \ --secret-id "openai/codex" \ --region "${AWS_REGION:-us-west-2}" \ --query SecretString \ - --output text) + --output text 2>&1); then + echo "ERROR: Failed to fetch secret 'openai/codex' from Secrets Manager" + echo "Please create the secret first:" + echo " aws secretsmanager create-secret \\" + echo " --name 'openai/codex' \\" + echo " --secret-string '{\"api_key\":\"sk-YOUR-KEY\"}' \\" + echo " --region ${AWS_REGION:-us-west-2}" + exit 1 +fi export CODEX_API_KEY=$(echo "$SECRET" | jq -r .api_key) +if [ -z "$CODEX_API_KEY" ] || [ "$CODEX_API_KEY" = "null" ]; then + echo "ERROR: api_key not found in secret. Expected format: {\"api_key\":\"sk-...\"}" + exit 1 +fi + echo "Credentials loaded. Starting server..." exec node /app/server.js diff --git a/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/invoke.py b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/invoke.py index 7542ee4c4..16f4e7347 100644 --- a/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/invoke.py +++ b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/invoke.py @@ -1,5 +1,5 @@ """ -Invoke the Claude Code agent deployed on AgentCore Runtime. +Invoke the Codex CLI agent deployed on AgentCore Runtime. Usage: python invoke.py diff --git a/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/update.py b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/update.py index bd310a9a8..2524af14f 100644 --- a/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/update.py +++ b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/update.py @@ -79,7 +79,7 @@ def update_runtime(runtime_id: str, role_arn: str) -> dict: }, }, protocolConfiguration={"serverProtocol": PROTOCOL}, - description="Claude Code agent on AgentCore Runtime with S3 Files", + description="Codex CLI agent on AgentCore Runtime with S3 Files, backed by OpenAI gpt-4o", ) if S3FILES_AP_ARN: From ca25b905f3eb879245fb5be48fdf70a08a3a9a25 Mon Sep 17 00:00:00 2001 From: Sushant20 Date: Tue, 19 May 2026 05:27:35 +0000 Subject: [PATCH 4/4] updated IAM role to be per stack --- .../03-codex-with-s3-files-openai-api-key/cfn-vpc.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/cfn-vpc.yaml b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/cfn-vpc.yaml index 8a7d3673c..d92bef29f 100644 --- a/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/cfn-vpc.yaml +++ b/01-tutorials/01-AgentCore-runtime/12-coding-agents/03-codex-with-s3-files-openai-api-key/cfn-vpc.yaml @@ -231,7 +231,7 @@ Resources: S3FilesRole: Type: AWS::IAM::Role Properties: - RoleName: !Sub "s3files-${BucketName}-role" + RoleName: !Sub "s3files-${AWS::StackName}-role" AssumeRolePolicyDocument: Version: "2012-10-17" Statement: