diff --git a/01-tutorials/10-Agent-Registry/01-advanced/admin-approval-workflow/IAM_PERMISSIONS.md b/01-tutorials/10-Agent-Registry/01-advanced/admin-approval-workflow/IAM_PERMISSIONS.md new file mode 100644 index 000000000..2980b0f9e --- /dev/null +++ b/01-tutorials/10-Agent-Registry/01-advanced/admin-approval-workflow/IAM_PERMISSIONS.md @@ -0,0 +1,266 @@ +# IAM Permissions for Admin Approval Workflow sample + +Create IAM user or role with the following permissions. + +> **Before using these policies**, replace every occurrence of `YOUR_ACCOUNT_ID` with your 12-digit AWS account ID. +> Run the following command to find it: +> ```bash +> aws sts get-caller-identity --query Account --output text +> ``` +> Then do a find-and-replace of `YOUR_ACCOUNT_ID` in the JSON below before attaching the policy. + +## Policy for AWS Agent Registry access (Administrator) + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowCreatingAndListingRegistries", + "Effect": "Allow", + "Action": [ + "bedrock-agentcore:CreateRegistry", + "bedrock-agentcore:ListRegistries" + ], + "Resource": ["arn:aws:bedrock-agentcore:*:YOUR_ACCOUNT_ID:*"] + }, + { + "Sid": "AllowGetUpdateDeleteRegistry", + "Effect": "Allow", + "Action": [ + "bedrock-agentcore:GetRegistry", + "bedrock-agentcore:UpdateRegistry", + "bedrock-agentcore:DeleteRegistry" + ], + "Resource": ["arn:aws:bedrock-agentcore:*:YOUR_ACCOUNT_ID:registry/*"] + }, + { + "Sid": "AllowCreatingAndListingRegistryRecords", + "Effect": "Allow", + "Action": [ + "bedrock-agentcore:CreateRegistryRecord", + "bedrock-agentcore:ListRegistryRecords" + ], + "Resource": ["arn:aws:bedrock-agentcore:*:YOUR_ACCOUNT_ID:registry/*"] + }, + { + "Sid": "AllowRecordLevelOperations", + "Effect": "Allow", + "Action": [ + "bedrock-agentcore:GetRegistryRecord", + "bedrock-agentcore:UpdateRegistryRecord", + "bedrock-agentcore:DeleteRegistryRecord", + "bedrock-agentcore:SubmitRegistryRecordForApproval" + ], + "Resource": ["arn:aws:bedrock-agentcore:*:YOUR_ACCOUNT_ID:registry/*/record/*"] + }, + { + "Sid": "AllowApproveRejectDeprecateRecords", + "Effect": "Allow", + "Action": ["bedrock-agentcore:UpdateRegistryRecordStatus"], + "Resource": ["arn:aws:bedrock-agentcore:*:YOUR_ACCOUNT_ID:registry/*/record/*"] + }, + { + "Sid": "AdditionalPermissionForRegistryManagedWorkloadIdentity", + "Effect": "Allow", + "Action": ["bedrock-agentcore:*WorkloadIdentity"], + "Resource": ["arn:aws:bedrock-agentcore:*:YOUR_ACCOUNT_ID:workload-identity-directory/default/workload-identity/*"] + } + ] +} +``` + +## Policy for AWS Agent Registry access (Publisher) + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowListingAllRegistries", + "Effect": "Allow", + "Action": ["bedrock-agentcore:ListRegistries"], + "Resource": ["arn:aws:bedrock-agentcore:*:YOUR_ACCOUNT_ID:*"] + }, + { + "Sid": "AllowGetRegistry", + "Effect": "Allow", + "Action": ["bedrock-agentcore:GetRegistry"], + "Resource": ["arn:aws:bedrock-agentcore:*:YOUR_ACCOUNT_ID:registry/*"] + }, + { + "Sid": "AllowCreatingAndListingRegistryRecords", + "Effect": "Allow", + "Action": [ + "bedrock-agentcore:CreateRegistryRecord", + "bedrock-agentcore:ListRegistryRecords" + ], + "Resource": ["arn:aws:bedrock-agentcore:*:YOUR_ACCOUNT_ID:registry/*"] + }, + { + "Sid": "AllowRecordLevelOperations", + "Effect": "Allow", + "Action": [ + "bedrock-agentcore:GetRegistryRecord", + "bedrock-agentcore:UpdateRegistryRecord", + "bedrock-agentcore:DeleteRegistryRecord", + "bedrock-agentcore:SubmitRegistryRecordForApproval" + ], + "Resource": ["arn:aws:bedrock-agentcore:*:YOUR_ACCOUNT_ID:registry/*/record/*"] + } + ] +} +``` + +## Permissions Required to deploy the required CI/CD stack such as DynamoDB and AWS Lambda etc. + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "STSCallerIdentity", + "Effect": "Allow", + "Action": ["sts:GetCallerIdentity"], + "Resource": "*" + }, + { + "Sid": "CloudFormationValidate", + "Effect": "Allow", + "Action": ["cloudformation:ValidateTemplate"], + "Resource": "*" + }, + { + "Sid": "CloudFormationStackManagement", + "Effect": "Allow", + "Action": [ + "cloudformation:CreateStack", + "cloudformation:UpdateStack", + "cloudformation:DeleteStack", + "cloudformation:DescribeStacks", + "cloudformation:DescribeStackEvents", + "cloudformation:DescribeStackResources", + "cloudformation:GetTemplate", + "cloudformation:ListStackResources", + "cloudformation:CreateChangeSet", + "cloudformation:DescribeChangeSet", + "cloudformation:ExecuteChangeSet", + "cloudformation:DeleteChangeSet" + ], + "Resource": "arn:aws:cloudformation:*:YOUR_ACCOUNT_ID:stack/*/*" + }, + { + "Sid": "S3StagingBucketManagement", + "Effect": "Allow", + "Action": [ + "s3:CreateBucket", + "s3:DeleteBucket", + "s3:HeadBucket", + "s3:PutBucketPublicAccessBlock", + "s3:GetBucketPublicAccessBlock", + "s3:ListBucket", + "s3:DeleteObject", + "s3:PutObject", + "s3:GetObject" + ], + "Resource": [ + "arn:aws:s3:::*", + "arn:aws:s3:::*/*" + ] + }, + { + "Sid": "LambdaFunctionManagement", + "Effect": "Allow", + "Action": [ + "lambda:CreateFunction", + "lambda:UpdateFunctionCode", + "lambda:UpdateFunctionConfiguration", + "lambda:DeleteFunction", + "lambda:GetFunction", + "lambda:GetFunctionConfiguration", + "lambda:AddPermission", + "lambda:RemovePermission" + ], + "Resource": "arn:aws:lambda:*:YOUR_ACCOUNT_ID:function:*" + }, + { + "Sid": "LambdaLayerManagement", + "Effect": "Allow", + "Action": [ + "lambda:PublishLayerVersion", + "lambda:DeleteLayerVersion", + "lambda:GetLayerVersion", + "lambda:ListLayerVersions" + ], + "Resource": "arn:aws:lambda:*:YOUR_ACCOUNT_ID:layer:*" + }, + { + "Sid": "IAMRoleManagement", + "Effect": "Allow", + "Action": [ + "iam:CreateRole", + "iam:DeleteRole", + "iam:GetRole", + "iam:PassRole", + "iam:AttachRolePolicy", + "iam:DetachRolePolicy", + "iam:PutRolePolicy", + "iam:DeleteRolePolicy", + "iam:GetRolePolicy", + "iam:ListRolePolicies", + "iam:ListAttachedRolePolicies" + ], + "Resource": "arn:aws:iam::YOUR_ACCOUNT_ID:role/*" + }, + { + "Sid": "KMSCreateKey", + "Effect": "Allow", + "Action": ["kms:CreateKey"], + "Resource": "*" + }, + { + "Sid": "KMSManageTaggedKeys", + "Effect": "Allow", + "Action": [ + "kms:DescribeKey", + "kms:EnableKeyRotation", + "kms:GetKeyPolicy", + "kms:PutKeyPolicy", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:TagResource", + "kms:UntagResource" + ], + "Resource": "*" + }, + { + "Sid": "DynamoDBTableManagement", + "Effect": "Allow", + "Action": [ + "dynamodb:CreateTable", + "dynamodb:DeleteTable", + "dynamodb:DescribeTable", + "dynamodb:UpdateTable", + "dynamodb:DescribeContinuousBackups", + "dynamodb:DescribeTimeToLive" + ], + "Resource": "arn:aws:dynamodb:*:YOUR_ACCOUNT_ID:table/*" + }, + { + "Sid": "EventBridgeManagement", + "Effect": "Allow", + "Action": [ + "events:PutRule", + "events:DeleteRule", + "events:DescribeRule", + "events:PutTargets", + "events:RemoveTargets", + "events:ListTargetsByRule" + ], + "Resource": "arn:aws:events:*:YOUR_ACCOUNT_ID:rule/*" + } + ] +} +``` + + diff --git a/01-tutorials/10-Agent-Registry/01-advanced/admin-approval-workflow/README.md b/01-tutorials/10-Agent-Registry/01-advanced/admin-approval-workflow/README.md index 9aeddecf9..f72e14521 100644 --- a/01-tutorials/10-Agent-Registry/01-advanced/admin-approval-workflow/README.md +++ b/01-tutorials/10-Agent-Registry/01-advanced/admin-approval-workflow/README.md @@ -67,7 +67,7 @@ As an Administrator, you can use the **AWS CLI** commands included in the notifi ## Prerequisites -- IAM credentials with appropriate permissions (see [`IAM_PERMISSIONS.md`](../../IAM_PERMISSIONS.md)). This tutorial requires both admin and publisher permissions. In addition, the following permissions are required to deploy and destroy the CI/CD stack: +- IAM credentials with appropriate permissions (see [`IAM_PERMISSIONS.md`](./IAM_PERMISSIONS.md)). In addition to Agent Registry related operations, the following permissions are being used: | Service | Permissions | |:--------|:------------| diff --git a/01-tutorials/10-Agent-Registry/01-advanced/admin-approval-workflow/admin-approval-workflow-notebook.ipynb b/01-tutorials/10-Agent-Registry/01-advanced/admin-approval-workflow/admin-approval-workflow-notebook.ipynb index 2830d7c4b..4e646fa09 100644 --- a/01-tutorials/10-Agent-Registry/01-advanced/admin-approval-workflow/admin-approval-workflow-notebook.ipynb +++ b/01-tutorials/10-Agent-Registry/01-advanced/admin-approval-workflow/admin-approval-workflow-notebook.ipynb @@ -59,17 +59,19 @@ "metadata": {}, "source": [ "## Prerequisites\n", - "- IAM credentials with appropriate permissions (see [`IAM_PERMISSIONS.md`](../../IAM_PERMISSIONS.md)). This tutorial requires both admin and publisher permissions. In addition, the following permissions are required to deploy and destroy the CI/CD stack:\n", - "\n", - " | Service | Permissions |\n", - " |:--------|:------------|\n", - " | **Amazon S3** | `CreateBucket`, `HeadBucket`, `PutPublicAccessBlock`, `DeleteBucket`, `ListBucket`, `PutObject`, `GetObject`, `DeleteObject` |\n", - " | **AWS CloudFormation** | `CreateStack`, `UpdateStack`, `DeleteStack`, `DescribeStacks`, `CreateChangeSet`, `ExecuteChangeSet`, `DescribeChangeSet`, `DeleteChangeSet` |\n", - " | **AWS Lambda** | `CreateFunction`, `UpdateFunctionCode`, `UpdateFunctionConfiguration`, `GetFunction`, `DeleteFunction`, `PublishLayerVersion`, `DeleteLayerVersion`, `AddPermission`, `RemovePermission` |\n", - " | **AWS IAM** | `CreateRole`, `GetRole`, `DeleteRole`, `PassRole`, `AttachRolePolicy`, `DetachRolePolicy`, `PutRolePolicy`, `DeleteRolePolicy` |\n", - " | **AWS EventBridge** | `PutRule`, `DescribeRule`, `DeleteRule`, `PutTargets`, `RemoveTargets` |\n", - " | **Amazon DynamoDB** | `CreateTable`, `DeleteTable`, `DescribeTable` |\n", - " | **AWS CloudWatch Logs** | `CreateLogGroup`, `CreateLogStream`, `PutLogEvents`, `DeleteLogGroup` |- Python 3.9+ with `boto3` installed\n", + "- IAM credentials with appropriate permissions (see [`IAM_PERMISSIONS.md`](./IAM_PERMISSIONS.md)). In addition to Agent Registry related operations, the following permissions are being used:\n", + "\n", + "| Service | Permissions |\n", + "|:--------|:------------|\n", + "| **Amazon S3** | `CreateBucket`, `HeadBucket`, `PutPublicAccessBlock`, `DeleteBucket`, `ListBucket`, `PutObject`, `GetObject`, `DeleteObject` |\n", + "| **AWS CloudFormation** | `CreateStack`, `UpdateStack`, `DeleteStack`, `DescribeStacks`, `CreateChangeSet`, `ExecuteChangeSet`, `DescribeChangeSet`, `DeleteChangeSet` |\n", + "| **AWS Lambda** | `CreateFunction`, `UpdateFunctionCode`, `UpdateFunctionConfiguration`, `GetFunction`, `DeleteFunction`, `PublishLayerVersion`, `DeleteLayerVersion`, `AddPermission`, `RemovePermission` |\n", + "| **AWS IAM** | `CreateRole`, `GetRole`, `DeleteRole`, `PassRole`, `AttachRolePolicy`, `DetachRolePolicy`, `PutRolePolicy`, `DeleteRolePolicy` |\n", + "| **AWS EventBridge** | `PutRule`, `DescribeRule`, `DeleteRule`, `PutTargets`, `RemoveTargets` |\n", + "| **Amazon DynamoDB** | `CreateTable`, `DeleteTable`, `DescribeTable` |\n", + "| **AWS CloudWatch Logs** | `CreateLogGroup`, `CreateLogStream`, `PutLogEvents`, `DeleteLogGroup` |\n", + "\n", + "- Python 3.9+ with `boto3` installed\n", "\n", "- [uv](https://docs.astral.sh/uv/getting-started/installation/) package manager (for installing python dependencies)\n", "- A Slack workspace with an [incoming webhook](https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/) configured. Note down the webhook URL and channel name.\n", @@ -98,6 +100,8 @@ "import subprocess\n", "import botocore.exceptions\n", "import time\n", + "import os\n", + "from utils import wait_for_registry_ready, wait_for_record_draft\n", "\n", "print(f\"Boto3 version: {boto3.__version__}\")\n", "\n", @@ -159,19 +163,8 @@ "metadata": {}, "outputs": [], "source": [ - "#wait for registry to get to Ready status (takes around ~2 minutes)\n", - "registryStatus = 'Checking'\n", - "while registryStatus.lower() != 'ready':\n", - " getRegistry = cp_client.get_registry(registryId=REGISTRY_ID)\n", - "\n", - " registryStatus = getRegistry['status']\n", - "\n", - " if registryStatus.lower() == 'ready':\n", - " print(\"Verified: Registry is in Ready state\")\n", - " else:\n", - " print(f\"Registry is in {registryStatus} state. Waiting for it to be in Ready state\")\n", - "\n", - " time.sleep(10)" + "# wait for registry to get to Ready status (takes around ~2 minutes)\n", + "wait_for_registry_ready(cp_client, REGISTRY_ID)" ] }, { @@ -307,19 +300,8 @@ "outputs": [], "source": [ "# Wait for the record to be in Draft state\n", - "recordStatus = 'Checking'\n", - "while recordStatus.lower() != 'draft':\n", - " getRegistryRecord = cp_client.get_registry_record(registryId=REGISTRY_ID, recordId=A2A_RECORD_ID)\n", - " recordStatus = getRegistryRecord['status']\n", - " metadata = getRegistryRecord.get(\"ResponseMetadata\", {})\n", - "\n", - " if recordStatus.lower() == 'draft':\n", - " print(\"Verified: Registry record is in Draft state. Ready to be submitted for Approval\")\n", - " print(f\"Metadata | RequestId: {metadata['HTTPHeaders']['x-amzn-requestid']}, Timestamp: {metadata['HTTPHeaders']['date']}\")\n", - " else:\n", - " print(f\"Registry record is in {recordStatus} state. Waiting for it to be in Draft state\")\n", - "\n", - " time.sleep(2)\n", + "wait_for_record_draft(cp_client, REGISTRY_ID, A2A_RECORD_ID)\n", + "\n", "submit_resp = cp_client.submit_registry_record_for_approval(registryId=REGISTRY_ID, recordId=A2A_RECORD_ID)\n", "print(\"Record submitted for approval\")\n", "\n", @@ -381,19 +363,7 @@ "outputs": [], "source": [ "# Wait for the record to be in Draft state\n", - "recordStatus = 'Checking'\n", - "while recordStatus.lower() != 'draft':\n", - " getRegistryRecord = cp_client.get_registry_record(registryId=REGISTRY_ID, recordId=MCP_RECORD_ID)\n", - " recordStatus = getRegistryRecord['status']\n", - " metadata = getRegistryRecord.get(\"ResponseMetadata\", {})\n", - "\n", - " if recordStatus.lower() == 'draft':\n", - " print(\"Verified: Registry record is in Draft state. Ready to be submitted for Approval\")\n", - " print(f\"RequestId: {metadata['HTTPHeaders']['x-amzn-requestid']}, Timestamp: {metadata['HTTPHeaders']['date']}\")\n", - " else:\n", - " print(f\"Registry record is in {recordStatus} state. Waiting for it to be in Draft state\")\n", - "\n", - " time.sleep(2)\n", + "wait_for_record_draft(cp_client, REGISTRY_ID, MCP_RECORD_ID)\n", "\n", "#Submit for approval\n", "submit_resp = cp_client.submit_registry_record_for_approval(registryId=REGISTRY_ID, recordId=MCP_RECORD_ID)\n", @@ -448,19 +418,7 @@ "outputs": [], "source": [ "# Wait for the record to be in Draft state\n", - "recordStatus = 'Checking'\n", - "while recordStatus.lower() != 'draft':\n", - " getRegistryRecord = cp_client.get_registry_record(registryId=REGISTRY_ID, recordId=CUSTOM_RECORD_ID)\n", - " recordStatus = getRegistryRecord['status']\n", - " metadata = getRegistryRecord.get(\"ResponseMetadata\", {})\n", - "\n", - " if recordStatus.lower() == 'draft':\n", - " print(\"Verified: Registry record is in Draft state. Ready to be submitted for Approval\")\n", - " print(f\"RequestId: {metadata['HTTPHeaders']['x-amzn-requestid']}, Timestamp: {metadata['HTTPHeaders']['date']}\")\n", - " else:\n", - " print(f\"Registry record is in {recordStatus} state. Waiting for it to be in Draft state\")\n", - "\n", - " time.sleep(2)\n", + "wait_for_record_draft(cp_client, REGISTRY_ID, CUSTOM_RECORD_ID)\n", "\n", "#Submit for approval\n", "submit_resp = cp_client.submit_registry_record_for_approval(registryId=REGISTRY_ID, recordId=CUSTOM_RECORD_ID)\n", diff --git a/01-tutorials/10-Agent-Registry/01-advanced/admin-approval-workflow/utils.py b/01-tutorials/10-Agent-Registry/01-advanced/admin-approval-workflow/utils.py new file mode 100644 index 000000000..77e765971 --- /dev/null +++ b/01-tutorials/10-Agent-Registry/01-advanced/admin-approval-workflow/utils.py @@ -0,0 +1,40 @@ +"""Utility helpers for Agent Registry polling operations.""" + +import time + + +def wait_for_registry_ready(cp_client, registry_id, poll_interval=10): + """Poll until the registry reaches READY status.""" + status = "Checking" + while status.lower() != "ready": + resp = cp_client.get_registry(registryId=registry_id) + status = resp["status"] + if status.lower() == "ready": + print("Verified: Registry is in Ready state") + else: + print(f"Registry is in {status} state. Waiting for it to be in Ready state") + time.sleep(poll_interval) + + +def wait_for_record_draft(cp_client, registry_id, record_id, poll_interval=2): + """Poll until the registry record reaches DRAFT status.""" + status = "Checking" + while status.lower() != "draft": + resp = cp_client.get_registry_record(registryId=registry_id, recordId=record_id) + status = resp["status"] + metadata = resp.get("ResponseMetadata", {}) + if status.lower() == "draft": + print( + "Verified: Registry record is in Draft state. " + "Ready to be submitted for Approval" + ) + headers = metadata.get("HTTPHeaders", {}) + request_id = headers.get("x-amzn-requestid", "") + date = headers.get("date", "") + print(f"RequestId: {request_id}, Timestamp: {date}") + else: + print( + f"Registry record is in {status} state. " + "Waiting for it to be in Draft state" + ) + time.sleep(poll_interval)