diff --git a/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/01-create-user-personas-workflow.ipynb b/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/01-create-user-personas-workflow.ipynb new file mode 100644 index 000000000..e6366dadc --- /dev/null +++ b/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/01-create-user-personas-workflow.ipynb @@ -0,0 +1,689 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 01 — User Personas and IAM Setup\n", + "\n", + "This notebook provides instructions for provisioning IAM persona roles required to interact with AWS Agent Registry. The roles facilitate core registry operations: registry creation, publishing records, approvals, and search functionality.\n", + "\n", + "## What You'll Learn\n", + "\n", + "- What AWS Agent Registry is\n", + "- The persona roles (Admin, Publisher, Consumer) and why separation of duties matters\n", + "- How to create IAM roles with scoped permissions for each persona\n", + "- How to assume them\n", + "- How to verify the setup before moving onto the next Getting Started notebook (02*)\n", + "\n", + "\n", + "\n", + "---\n", + "\n", + "### Use Case: Enterprise Payment Processing\n", + "\n", + "#### Overview\n", + "\n", + "AnyCompany runs an e-commerce platform that handles thousands of daily transactions alongside a lending arm that processes loan applications, credit checks, and installment plans. Rather than building separate integrations for every AI agent across both domains, they want a central registry where payment and loan processing capabilities are published and discoverable by any agent at runtime.\n", + "\n", + "#### Business Context\n", + "\n", + "| | |\n", + "|---|---|\n", + "| **Challenge** | Customer service agents need real-time payment processing capabilities, refund handling, and transaction status lookup|\n", + "| **Goal** | Centralize payment processing capabilities that any AI agent can discover and use |\n", + "| **Benefit** | Reduce integration complexity, ensure consistent payment handling, and enable rapid deployment of agents, skills, MCP and custom tools |" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Solution: What is AWS Agent Registry?\n", + "\n", + "[AWS Agent Registry](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/registry.html#registry-what-is) is a unified catalog for discovering, governing, and managing agents & tools. \n", + "\n", + "#### Core Capabilities\n", + "\n", + "- **Discovery:** Hybrid search (keyword & semantic) for discovering agents and tools. Keyword filtering on structured attributes.\n", + "- **Governance:** Integrate approval workflows for registered agents and tools  and apply policy validations for registered agents.\n", + "- **Semantic Search:** Consumers can find the right tools by describing what they need.\n", + "- **Security:** Secure access to view/update Registry and Registry records. Cross-account Registry access with IAM and OAuth support.\n", + "- **Observability:** Usage statistics, performance metrics, and error rates. Audit trails for compliance (⚠️ Coming Soon)\n", + "\n", + "## End-to-End Workflow\n", + "The diagram below shows the end-to-end workflow:\n", + "\n", + "1. **Admin creates a registry**\n", + "2. **Publisher creates registry records** containing metadata for A2A agents, MCP tools, Agent Skills or custom tools\n", + "3. **Admin approves or rejects** the submitted records\n", + "4. **Consumer searches and discovers** approved records via the AWS Console, Kiro, or SDK\n", + "\n", + "![AgentCore Registry Overview](images/registry-architecture.png)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Registry Persona Roles\n", + "\n", + "The workshop uses three IAM roles to demonstrate separation of duties:\n", + "\n", + "| Persona | Role Name | Purpose | Notebooks |\n", + "|---------|-----------|---------|----------|\n", + "| **Admin** | `admin_persona` | Creates registries, approves/rejects records, manages workload identities | 02, 04 |\n", + "| **Publisher** | `publisher_persona` | Creates and submits registry records for approval | 03 |\n", + "| **Consumer** | `consumer_persona` | Searches and discovers approved records | 05 |\n", + "\n", + "Each role has only the permissions it needs — no more, no less." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Notebook Series\n", + "\n", + "```\n", + "01 IAM Setup Create persona roles and permissions\n", + " └─▶ 02 Create Registry Admin creates a registry with approval workflow\n", + " └─▶ 03 Publish Records Publisher submits tool records\n", + " └─▶ 04 Approval Workflows Admin approves/rejects submissions\n", + " └─▶ 05 Semantic Search Consumer discovers approved tools\n", + "```\n", + "\n", + "Run the notebooks in the order above. This **notebook (01)** must be completed first." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## How Your Current Identity Is Used\n", + "\n", + "This notebook detects your identity (IAM role / user, sagemaker role) you are using and:\n", + "\n", + "1. Creates three IAM Roles (Admin, Publisher, Consumer)\n", + "2. Adds your current identity as a **trusted principal** in each persona role's trust policy, allowing you to call `sts:AssumeRole` to switch into any of them\n", + "3. Attaches an **inline permissions policy** to each persona role granting scoped `bedrock-agentcore` actions\n", + "\n", + "Each subsequent notebook (02–05) assumes the relevant persona role via `sts.assume_role()`, so every API call runs with only the permissions that persona needs — mirroring how separate teams would operate in production." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "- **boto3 >= 1.42.87**\n", + "- **IAM permissions** — Your IAM user or role needs the ability to create roles, attach policies, and assume them. The minimum policy required is shown below.\n", + "\n", + "> **Note:** `bedrock-agentcore` permissions are not needed on your identity — they are attached to the persona roles that this notebook creates. Your identity only needs IAM management and STS permissions.\n", + "\n", + "```json\n", + "{\n", + " \"Version\": \"2012-10-17\",\n", + " \"Statement\": [\n", + " {\n", + " \"Sid\": \"IAMRoleManagement\",\n", + " \"Effect\": \"Allow\",\n", + " \"Action\": [\n", + " \"iam:CreateRole\",\n", + " \"iam:GetRole\",\n", + " \"iam:PutRolePolicy\",\n", + " \"iam:UpdateAssumeRolePolicy\",\n", + " \"iam:DeleteRole\",\n", + " \"iam:DeleteRolePolicy\",\n", + " \"iam:ListRolePolicies\",\n", + " \"iam:ListAttachedRolePolicies\",\n", + " \"iam:DetachRolePolicy\",\n", + " \"iam:ListInstanceProfilesForRole\",\n", + " \"iam:RemoveRoleFromInstanceProfile\",\n", + " \"iam:PassRole\"\n", + " ],\n", + " \"Resource\": \"arn:aws:iam::*:role/*\"\n", + " },\n", + " {\n", + " \"Sid\": \"STS\",\n", + " \"Effect\": \"Allow\",\n", + " \"Action\": [\n", + " \"sts:AssumeRole\",\n", + " \"sts:GetCallerIdentity\"\n", + " ],\n", + " \"Resource\": \"*\"\n", + " }\n", + " ]\n", + "}\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 1. Install boto3 SDK and dependencies" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Install the required Python packages." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install boto3 python-dotenv --force-reinstall" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Auto-Detect Current Identity\n", + "\n", + "This works with both **Amazon SageMaker notebooks** (which have a role attached automatically) and environments with configured AWS credentials. If you are not using a SageMaker notebook, make sure your AWS credentials are set (e.g., via `aws configure`, environment variables, or a credentials file).\n", + "\n", + "Detect the current caller identity and extract the IAM principal ARN. If running as an assumed role or service role (e.g., SageMaker), the ARN is resolved to the base IAM role ARN format required by trust policies." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import boto3\n", + "import json\n", + "import time\n", + "import botocore.exceptions\n", + "import utils\n", + "import os\n", + "\n", + "AWS_REGION = os.environ.get(\"AWS_DEFAULT_REGION\", \"us-west-2\")\n", + "ACCOUNT_ID = ''\n", + "try:\n", + " sts = boto3.client(\"sts\", region_name=AWS_REGION)\n", + " identity = sts.get_caller_identity()\n", + " ACCOUNT_ID = identity[\"Account\"]\n", + " CALLER_ARN = identity[\"Arn\"]\n", + " print(f\"Account ID : {ACCOUNT_ID}\")\n", + " print(f\"Caller ARN : {CALLER_ARN}\")\n", + "except (botocore.exceptions.NoCredentialsError, botocore.exceptions.ClientError) as e:\n", + " print(f\"ERROR: Could not retrieve caller identity — {e}\")\n", + " print(\"Make sure your AWS credentials are configured.\")\n", + " raise SystemExit(1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Resolve the caller ARN to the base IAM principal ARN. This handles assumed-role and service-role ARN formats." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ROLE_ARN = utils.extract_role_arn(CALLER_ARN)\n", + "print(f\"Principal ARN: {ROLE_ARN}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create an IAM client for role and policy management." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "iam_client = boto3.client(\"iam\", region_name=AWS_REGION)\n", + "print(f\"IAM client ready — region: {AWS_REGION}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Create Persona Roles" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Define Persona Permissions\n", + "\n", + "Each persona is defined with a policy name and a list of allowed `bedrock-agentcore` actions. The **admin** has full registry control, the **publisher** can create and submit records, and the **consumer** can only search and read." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "PERSONA_DEFINITIONS = {\n", + " \"admin_persona\": {\n", + " \"policy_name\": \"AdminPolicy\",\n", + " \"actions\": [\n", + " \"bedrock-agentcore:CreateRegistry\",\n", + " \"bedrock-agentcore:ListRegistries\",\n", + " \"bedrock-agentcore:GetRegistry\",\n", + " \"bedrock-agentcore:UpdateRegistry\",\n", + " \"bedrock-agentcore:DeleteRegistry\",\n", + " \"bedrock-agentcore:CreateRegistryRecord\",\n", + " \"bedrock-agentcore:ListRegistryRecords\",\n", + " \"bedrock-agentcore:GetRegistryRecord\",\n", + " \"bedrock-agentcore:UpdateRegistryRecord\",\n", + " \"bedrock-agentcore:DeleteRegistryRecord\",\n", + " \"bedrock-agentcore:SubmitRegistryRecordForApproval\",\n", + " \"bedrock-agentcore:UpdateRegistryRecordStatus\",\n", + " \"bedrock-agentcore:*WorkloadIdentity\",\n", + " ],\n", + " },\n", + " \"publisher_persona\": {\n", + " \"policy_name\": \"PublisherPolicy\",\n", + " \"actions\": [\n", + " \"bedrock-agentcore:ListRegistries\",\n", + " \"bedrock-agentcore:GetRegistry\",\n", + " \"bedrock-agentcore:CreateRegistryRecord\",\n", + " \"bedrock-agentcore:ListRegistryRecords\",\n", + " \"bedrock-agentcore:GetRegistryRecord\",\n", + " \"bedrock-agentcore:DeleteRegistryRecord\",\n", + " \"bedrock-agentcore:UpdateRegistryRecord\",\n", + " \"bedrock-agentcore:SubmitRegistryRecordForApproval\",\n", + " ],\n", + " },\n", + " \"consumer_persona\": {\n", + " \"policy_name\": \"ConsumerPolicy\",\n", + " \"actions\": [\n", + " \"bedrock-agentcore:ListRegistries\",\n", + " \"bedrock-agentcore:GetRegistry\",\n", + " \"bedrock-agentcore:GetRegistryRecord\",\n", + " \"bedrock-agentcore:ListRegistryRecords\",\n", + " \"bedrock-agentcore:SearchRegistryRecords\",\n", + " ],\n", + " },\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create or Update All Persona Roles\n", + "\n", + "Loop through each persona definition, create the role (or update if it exists), and attach the permissions policy. A brief pause between roles avoids IAM throttling." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "trust_policy = utils.build_trust_policy(ROLE_ARN)\n", + "persona_role_arns = {}\n", + "\n", + "for role_name, config in PERSONA_DEFINITIONS.items():\n", + " print(f\"\\n{'='*60}\")\n", + " print(f\"Setting up: {role_name}\")\n", + " print(f\"{'='*60}\")\n", + " role_arn = utils.create_or_update_persona_role(\n", + " iam_client,\n", + " role_name,\n", + " config[\"policy_name\"],\n", + " config[\"actions\"],\n", + " trust_policy,\n", + " ACCOUNT_ID\n", + " )\n", + " persona_role_arns[role_name] = role_arn\n", + " time.sleep(1) # Brief pause between role creations\n", + "\n", + "print(f\"\\n✅ All persona roles ready:\")\n", + "for name, arn in persona_role_arns.items():\n", + " print(f\" {name}: {arn}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 4. Verification — Test Assuming Each Persona Role\n", + "\n", + "We wait 10 seconds for IAM propagation, then test assuming each persona role. All three should succeed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Waiting 10 seconds for IAM propagation...\")\n", + "time.sleep(10)\n", + "print(\"Done.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Test Role Assumption\n", + "\n", + "Attempt to assume each persona role using sts.assume_role(). A successful assumption confirms the trust policy and AssumeRole permission are correctly configured." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "results = {}\n", + "\n", + "for role_name, role_arn in persona_role_arns.items():\n", + " print(f\"\\nAssuming {role_name} ({role_arn})...\")\n", + " try:\n", + " resp = utils.assume_role_only(AWS_REGION, role_arn, session_name=f\"{role_name}-verify\")\n", + " utils.pp(resp)\n", + " assumed_arn = resp[\"AssumedRoleUser\"][\"Arn\"]\n", + " expiration = resp[\"Credentials\"][\"Expiration\"]\n", + " print(f\" ✅ Success — assumed: {assumed_arn}\")\n", + " print(f\" Expires: {expiration}\")\n", + " results[role_name] = \"PASS\"\n", + " except botocore.exceptions.ClientError as e:\n", + " print(f\" ❌ Failed — {e}\")\n", + " print(f\" Try waiting a few more seconds for IAM propagation and re-run this cell.\")\n", + " results[role_name] = \"FAIL\"\n", + "\n", + "print(f\"\\n{'='*60}\")\n", + "print(\"Verification Summary\")\n", + "print(f\"{'='*60}\")\n", + "for name, status in results.items():\n", + " icon = \"✅\" if status == \"PASS\" else \"❌\"\n", + " print(f\" {icon} {name}: {status}\")\n", + "\n", + "if all(s == \"PASS\" for s in results.values()):\n", + " print(f\"\\n🎉 All roles verified! You are ready to proceed to notebook 02.\")\n", + "else:\n", + " print(f\"\\n⚠️ Some roles failed. Check the errors above and re-run after a brief wait.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 5. Manual IAM Setup (Alternative)\n", + "\n", + "If you cannot create roles programmatically (e.g., restricted environment), follow these steps in the AWS IAM console.\n", + "\n", + "### Step 1: Create each persona role\n", + "\n", + "For each of the three roles (`admin_persona`, `publisher_persona`, `consumer_persona`):\n", + "\n", + "1. Go to **IAM → Roles → Create role**\n", + "2. Select **Custom trust policy**\n", + "3. Paste the trust policy JSON below (replace `` with your IAM user or role ARN)\n", + "4. Click **Next**, then **Create policy** to add the permissions policy\n", + "5. Name the role exactly as shown\n", + "\n", + "### Trust Policy (same for all three roles)\n", + "\n", + "```json\n", + "{\n", + " \"Version\": \"2012-10-17\",\n", + " \"Statement\": [\n", + " {\n", + " \"Effect\": \"Allow\",\n", + " \"Principal\": {\n", + " \"Service\": \"bedrock-agentcore.amazonaws.com\"\n", + " },\n", + " \"Action\": \"sts:AssumeRole\"\n", + " },\n", + " {\n", + " \"Effect\": \"Allow\",\n", + " \"Principal\": {\n", + " \"AWS\": \"\"\n", + " },\n", + " \"Action\": \"sts:AssumeRole\"\n", + " }\n", + " ]\n", + "}\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 2: Attach permissions policies\n", + "\n", + "#### AdminPolicy (for `admin_persona`)\n", + "\n", + "```json\n", + "{\n", + " \"Version\": \"2012-10-17\",\n", + " \"Statement\": [\n", + " {\n", + " \"Effect\": \"Allow\",\n", + " \"Action\": [\n", + " \"bedrock-agentcore:CreateRegistry\",\n", + " \"bedrock-agentcore:ListRegistries\",\n", + " \"bedrock-agentcore:GetRegistry\",\n", + " \"bedrock-agentcore:UpdateRegistry\",\n", + " \"bedrock-agentcore:DeleteRegistry\",\n", + " \"bedrock-agentcore:CreateRegistryRecord\",\n", + " \"bedrock-agentcore:ListRegistryRecords\",\n", + " \"bedrock-agentcore:GetRegistryRecord\",\n", + " \"bedrock-agentcore:UpdateRegistryRecord\",\n", + " \"bedrock-agentcore:DeleteRegistryRecord\",\n", + " \"bedrock-agentcore:SubmitRegistryRecordForApproval\",\n", + " \"bedrock-agentcore:UpdateRegistryRecordStatus\",\n", + " \"bedrock-agentcore:*WorkloadIdentity\"\n", + " ],\n", + " \"Resource\": \"*\"\n", + " }\n", + " ]\n", + "}\n", + "```\n", + "\n", + "#### PublisherPolicy (for `publisher_persona`)\n", + "\n", + "```json\n", + "{\n", + " \"Version\": \"2012-10-17\",\n", + " \"Statement\": [\n", + " {\n", + " \"Effect\": \"Allow\",\n", + " \"Action\": [\n", + " \"bedrock-agentcore:ListRegistries\",\n", + " \"bedrock-agentcore:GetRegistry\",\n", + " \"bedrock-agentcore:CreateRegistryRecord\",\n", + " \"bedrock-agentcore:ListRegistryRecords\",\n", + " \"bedrock-agentcore:GetRegistryRecord\",\n", + " \"bedrock-agentcore:DeleteRegistryRecord\",\n", + " \"bedrock-agentcore:UpdateRegistryRecord\",\n", + " \"bedrock-agentcore:SubmitRegistryRecordForApproval\"\n", + " ],\n", + " \"Resource\": \"*\"\n", + " }\n", + " ]\n", + "}\n", + "```\n", + "\n", + "#### ConsumerPolicy (for `consumer_persona`)\n", + "\n", + "```json\n", + "{\n", + " \"Version\": \"2012-10-17\",\n", + " \"Statement\": [\n", + " {\n", + " \"Effect\": \"Allow\",\n", + " \"Action\": [\n", + " \"bedrock-agentcore:ListRegistries\",\n", + " \"bedrock-agentcore:GetRegistry\",\n", + " \"bedrock-agentcore:GetRegistryRecord\",\n", + " \"bedrock-agentcore:ListRegistryRecords\",\n", + " \"bedrock-agentcore:SearchRegistryRecords\"\n", + " ],\n", + " \"Resource\": \"*\"\n", + " }\n", + " ]\n", + "}\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 6. Cleanup\n", + "\n", + "⚠️ **WARNING: Only run this section after you have completed ALL notebooks (02–05).** Deleting these roles will break the other notebooks." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# cleanup_results = {}\n", + "\n", + "# for role_name, config in PERSONA_DEFINITIONS.items():\n", + "# print(f\"\\nCleaning up: {role_name}\")\n", + "# try:\n", + "# # 1. Delete ALL inline policies (not just the one we created)\n", + "# try:\n", + "# inline_policies = iam_client.list_role_policies(RoleName=role_name)\n", + "# for policy_name in inline_policies.get(\"PolicyNames\", []):\n", + "# iam_client.delete_role_policy(\n", + "# RoleName=role_name,\n", + "# PolicyName=policy_name,\n", + "# )\n", + "# print(f\" Deleted inline policy: {policy_name}\")\n", + "# except iam_client.exceptions.NoSuchEntityException:\n", + "# print(f\" No inline policies found\")\n", + "\n", + "# # 2. Detach managed policies\n", + "# try:\n", + "# attached = iam_client.list_attached_role_policies(RoleName=role_name)\n", + "# for policy in attached.get(\"AttachedPolicies\", []):\n", + "# iam_client.detach_role_policy(\n", + "# RoleName=role_name,\n", + "# PolicyArn=policy[\"PolicyArn\"],\n", + "# )\n", + "# print(f\" Detached managed policy: {policy['PolicyArn']}\")\n", + "# except iam_client.exceptions.NoSuchEntityException:\n", + "# pass\n", + "\n", + "# # 3. Remove from instance profiles\n", + "# try:\n", + "# profiles = iam_client.list_instance_profiles_for_role(RoleName=role_name)\n", + "# for profile in profiles.get(\"InstanceProfiles\", []):\n", + "# iam_client.remove_role_from_instance_profile(\n", + "# InstanceProfileName=profile[\"InstanceProfileName\"],\n", + "# RoleName=role_name,\n", + "# )\n", + "# print(f\" Removed from instance profile: {profile['InstanceProfileName']}\")\n", + "# except iam_client.exceptions.NoSuchEntityException:\n", + "# pass\n", + "\n", + "# # 4. Delete the role\n", + "# iam_client.delete_role(RoleName=role_name)\n", + "# print(f\" ✅ Deleted role: {role_name}\")\n", + "# cleanup_results[role_name] = \"DELETED\"\n", + "\n", + "# except iam_client.exceptions.NoSuchEntityException:\n", + "# print(f\" Role {role_name} not found — already absent\")\n", + "# cleanup_results[role_name] = \"ABSENT\"\n", + "# except botocore.exceptions.ClientError as e:\n", + "# print(f\" ❌ Error: {e}\")\n", + "# cleanup_results[role_name] = \"ERROR\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Cleanup Summary\n", + "\n", + "Display which resources were successfully deleted and which were already absent." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# print(f\"\\n{'='*60}\")\n", + "# print(\"Cleanup Summary\")\n", + "# print(f\"{'='*60}\")\n", + "# for name, status in cleanup_results.items():\n", + "# icon = \"✅\" if status == \"DELETED\" else \"⚪\" if status == \"ABSENT\" else \"❌\"\n", + "# print(f\" {icon} {name}: {status}\")\n", + "# print(f\"\\nCleanup complete.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## What happens next?\n", + "\n", + "Now that the three persona roles are created and verified, proceed to **notebook 02 — Creating a Registry** to create your first AWS Agent Registry as the admin persona.\n", + "\n", + "The roles created here will be used across all remaining notebooks:\n", + "\n", + "- **Notebook 02** — [Creating Registry](02-creating-registry-workflow.ipynb): Admin creates a registry\n", + "- **Notebook 03** — [Publishing Records](03-publishing-records-workflow.ipynb): Publish records as a Publisher\n", + "- **Notebook 04** — [Admin Approval](04-admin-approval-workflow.ipynb): Admin Approval workflow \n", + "- **Notebook 05** — [Semantic Search](05-search-registry-workflow.ipynb): Search approved records using NLQ as a Consumer" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "finalchangeenv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/02-creating-registry-workflow.ipynb b/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/02-creating-registry-workflow.ipynb new file mode 100644 index 000000000..e82201639 --- /dev/null +++ b/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/02-creating-registry-workflow.ipynb @@ -0,0 +1,398 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 02 - Creating Registry as Admin\n", + "\n", + "This notebook walks through the creation of a new AWS Agent Registry using Admin Persona and highlights different API operations associated with Registry. \n", + "\n", + "## What You'll Learn\n", + "\n", + "- Create and configure a **Registry** using admin persona\n", + "- List registries in your account\n", + "- Update registry configuration\n", + "\n", + "## Prerequisites\n", + "\n", + "- boto3 >= 1.42.87\n", + "- Execute [notebook 01](01-create-user-personas-workflow.ipynb) to create IAM roles for admin, publisher and consumer personas\n", + "\n", + "## Admin API References\n", + "\n", + "| # | API | Description |\n", + "|---|-----|-------------|\n", + "| 1 | [CreateRegistry](https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-agentcore-control/client/create_registry.html) | Create a new registry with approval configuration |\n", + "| 2 | [GetRegistry](https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-agentcore-control/client/get_registry.html) | Fetch registry details and poll for READY status |\n", + "| 3 | [ListRegistries](https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-agentcore-control/client/list_registries.html) | List all registries in the account |\n", + "| 4 | [UpdateRegistry](https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-agentcore-control/client/update_registry.html) | Update registry description or approval config |\n", + "| 5 | [DeleteRegistry](https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-agentcore-control/client/delete_registry.html) | Delete a registry by ID |\n", + "| 6 | [ListRegistryRecords](https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-agentcore-control/client/list_registry_records.html) | List all records in a registry (cleanup) |\n", + "| 7 | [DeleteRegistryRecord](https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-agentcore-control/client/delete_registry_record.html) | Delete a specific record from a registry (cleanup) |\n", + "\n", + "#### Notebook Chain\n", + "\n", + "**02 (this notebook)** → 03 (publish records) → 04 (admin approval) → 05 (semantic search)\n", + "\n", + "#### Use Case: Enterprise Payment Processing\n", + "**Admin Persona:** AnyCompany's administrator creates a centralized registry for payment processing agents and tools. This enables customer service AI agents to discover and use standardized payment, refund, and transaction capabilities — without requiring individual integrations per deployment." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 1. Install boto3 SDK and dependencies" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Install core dependencies (`boto3` and `python-dotenv`)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install boto3 python-dotenv --force-reinstall" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Initialize boto3 Session as Admin" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Assume the `admin_persona` IAM role and create a boto3 session with temporary credentials. All subsequent API calls use this session." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import boto3\n", + "import json\n", + "import time\n", + "import utils\n", + "import os\n", + "\n", + "AWS_REGION = os.environ.get(\"AWS_DEFAULT_REGION\", \"us-west-2\")\n", + "\n", + "# Auto-detect account ID from current credentials\n", + "sts = boto3.client(\"sts\", region_name=AWS_REGION)\n", + "ACCOUNT_ID = sts.get_caller_identity()[\"Account\"]\n", + "CALLER_ARN = sts.get_caller_identity()[\"Arn\"]\n", + "\n", + "ADMIN_ROLE_ARN = f\"arn:aws:iam::{ACCOUNT_ID}:role/admin_persona\"\n", + "\n", + "print(f\"Account: {ADMIN_ROLE_ARN}\")\n", + "\n", + "# Assume the Admin role\n", + "creds = utils.assume_role(\n", + " role_arn=ADMIN_ROLE_ARN,\n", + " session_name=\"admin-session\",\n", + ")\n", + "\n", + "admin_session = boto3.Session(\n", + " aws_access_key_id=creds[\"AccessKeyId\"],\n", + " aws_secret_access_key=creds[\"SecretAccessKey\"],\n", + " aws_session_token=creds[\"SessionToken\"],\n", + " region_name=AWS_REGION,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Initialize the Control Plane Client\n", + "\n", + "The control plane (`bedrock-agentcore-control`) handles CRUD operations for registries and records." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Control plane client (admin operations)\n", + "cp_client = admin_session.client(\"bedrock-agentcore-control\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 4. Create a Registry\n", + "\n", + "As an admin, you can create the registry that publishers can submit records to.\n", + "\n", + "Setting `autoApproval: False` is the default — records require explicit admin approval before becoming searchable." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "NEW_REGISTRY_NAME = \"AWSAgentRegistry\" # Change this\n", + "NEW_REGISTRY_DESCRIPTION = \"Registry created during the getting-started workshop\" # Change this\n", + "\n", + "print(f\"This will create registry: {NEW_REGISTRY_NAME}\\n\")\n", + "\n", + "resp = cp_client.create_registry(\n", + " name=NEW_REGISTRY_NAME,\n", + " description=NEW_REGISTRY_DESCRIPTION,\n", + " approvalConfiguration={\"autoApproval\": False},\n", + ")\n", + "\n", + "REGISTRY_ARN = resp[\"registryArn\"]\n", + "REGISTRY_ID = REGISTRY_ARN.split(\"/\")[-1]\n", + "\n", + "print(f\"Created registry: {NEW_REGISTRY_NAME} (ID: {REGISTRY_ID})\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4.1 Wait for Registry to be Ready\n", + "\n", + "Registry creation is asynchronous. Poll `GetRegistry` until the status transitions from `CREATING` to `READY`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "while True:\n", + " r = cp_client.get_registry(registryId=REGISTRY_ID)\n", + " if r[\"status\"] == \"READY\":\n", + " print(f\"Registry is READY\")\n", + " break\n", + " print(f\"Status: {r['status']} - waiting...\")\n", + " time.sleep(3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4.2 Inspect Registry Details\n", + "\n", + "Fetch the full registry object to confirm the name, description, approval configuration, and timestamps." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "registry = cp_client.get_registry(registryId=REGISTRY_ID)\n", + "utils.pp(registry)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "## 5. List Registries\n", + "\n", + "List all registries in your account. You can filter by status (`CREATING` or `READY`)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# List all registries\n", + "registries = cp_client.list_registries()\n", + "print(f\"Found {len(registries.get('registries', []))} registries:\\n\")\n", + "print(f\"{'#':<4} {'Name':<30} {'Registry ID':<20} {'Status':<18} {'Created At':<28} {'Updated At'}\")\n", + "print(\"-\" * 140)\n", + "for i, reg in enumerate(registries.get('registries', []), 1):\n", + " print(f\"{i:<4} {reg['name']:<30} {reg['registryId']:<20} {reg['status']:<18} {str(reg.get('createdAt','N/A')):<28} {str(reg.get('updatedAt','N/A'))}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "## 6. Update Registry Configuration\n", + "\n", + "The following example highlights changing Registry description." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "updated = cp_client.update_registry(\n", + " registryId=REGISTRY_ID,\n", + " description={\"optionalValue\": \"Registry created and updated\"}\n", + ")\n", + "\n", + "print(f\"Registry entry updated with New description: {updated['description']}\")\n", + "print(f\"Updated at: {updated['updatedAt']}\")\n", + "\n", + "registry = cp_client.get_registry(registryId=REGISTRY_ID)\n", + "utils.pp(registry)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 6.1 Verify the Update\n", + "\n", + "Re-fetch the registry to confirm the description change was applied." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "registry = cp_client.get_registry(registryId=REGISTRY_ID)\n", + "utils.pp(registry)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 7. Deleting a Registry and Its Records (Run with Caution)\n", + "\n", + "A registry can be deleted when it's in a terminal/settled state.\n", + "\n", + "Do not attempt to delete when it's in a transitional state like `CREATING`, `UPDATING`, or `DELETING` — the service is still processing and the delete call will likely fail or conflict.\n", + "\n", + "| Status | Type | Description |\n", + "|--------|------|-------------|\n", + "| `CREATING` | Transitional | Registry is being provisioned |\n", + "| `READY` | Terminal | Registry is active and usable |\n", + "| `UPDATING` | Transitional | A registry update is in progress |\n", + "| `DELETING` | Transitional | Registry deletion is in progress |\n", + "| `CREATE_FAILED` | Terminal | Registry provisioning failed |\n", + "| `UPDATE_FAILED` | Terminal | A registry update failed |\n", + "| `DELETE_FAILED` | Terminal | A registry deletion failed |" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "⚠️ Uncomment the code below to delete all records in the registry, then the registry itself." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# import botocore.exceptions\n", + "\n", + "# REGISTRY_ID = \"xa9Ms0EuuzddjpSF\" # You can update with specific Registry ID.\n", + "\n", + "# try:\n", + "# # Check registry status first\n", + "# reg = cp_client.get_registry(registryId=REGISTRY_ID)\n", + "# reg_status = reg.get(\"status\", \"\")\n", + "\n", + "# if reg_status == \"CREATE_FAILED\":\n", + "# cp_client.delete_registry(registryId=REGISTRY_ID)\n", + "# print(f\"Deleted registry in {reg_status} state.\")\n", + "# else:\n", + "# # Delete all records in the registry\n", + "# records = cp_client.list_registry_records(registryId=REGISTRY_ID)\n", + "# for rec in records[\"registryRecords\"]:\n", + "# cp_client.delete_registry_record(\n", + "# registryId=REGISTRY_ID,\n", + "# recordId=rec[\"recordId\"]\n", + "# )\n", + "# print(f\"Deleted record: {rec['recordId']}\")\n", + "\n", + "# # Delete the registry\n", + "# cp_client.delete_registry(registryId=REGISTRY_ID)\n", + "# print(f\"Deleted registry: {REGISTRY_ID}\")\n", + "\n", + "# print(\"\\nCleanup complete!\")\n", + "\n", + "# except cp_client.exceptions.ResourceNotFoundException:\n", + "# print(f\"Registry {REGISTRY_ID} not found - already cleaned up.\")\n", + "# except botocore.exceptions.ClientError as e:\n", + "# print(f\"Error during cleanup: {e}\")\n", + "\n", + "# # List all registries\n", + "# registries = cp_client.list_registries()\n", + "# print(f\"Found {len(registries.get('registries', []))} registries:\\n\")\n", + "# print(f\"{'#':<4} {'Name':<30} {'Registry ID':<20} {'Status':<18} {'Created At':<28} {'Updated At'}\")\n", + "# print(\"-\" * 140)\n", + "# for i, reg in enumerate(registries.get('registries', []), 1):\n", + "# print(f\"{i:<4} {reg['name']:<30} {reg['registryId']:<20} {reg['status']:<18} {str(reg.get('createdAt','N/A')):<28} {str(reg.get('updatedAt','N/A'))}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Pre-requisites\n", + "- **Notebook 01** — [Create User Personas](01-create-user-personas-workflow.ipynb): Set up user personas: admin, publisher, consumer\n", + "\n", + "## Next Steps\n", + "- **Notebook 03** — [Publishing Records](03-publishing-records-workflow.ipynb): Publish records as a Publisher\n", + "- **Notebook 04** — [Admin Approval](04-admin-approval-workflow.ipynb): Admin Approval workflow \n", + "- **Notebook 05** — [Semantic Search](05-search-registry-workflow.ipynb): Search approved records using NLQ as a Consumer" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "finalchangeenv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/03-publishing-records-workflow.ipynb b/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/03-publishing-records-workflow.ipynb new file mode 100644 index 000000000..1d27939ae --- /dev/null +++ b/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/03-publishing-records-workflow.ipynb @@ -0,0 +1,930 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 03 - Publishing Records as Publisher\n", + "\n", + "This notebook demonstrates the **Publisher** persona workflow for AWS Agent Registry.\n", + "A Publisher can create MCP/A2A/Custom/Agent Skills records, list and inspect records, update record content,\n", + "and submit records for admin approval — but **cannot** approve, reject, or deprecate records.\n", + "\n", + "### What You'll Learn \n", + "\n", + "1. **List & inspect** — Browse registries and records visible to the publisher\n", + "2. **Create records** — Creating MCP/A2A/CUSTOM records with metadata and descriptors\n", + "3. **Update records** — Modify descriptors on a DRAFT record\n", + "4. **Submit for approval** — Transition a record from DRAFT → PENDING_APPROVAL\n", + "\n", + "### Prerequisites\n", + "\n", + "- boto3 >= 1.42.87\n", + "- Execute [notebook 01](01-create-user-personas-workflow.ipynb) to create IAM roles for admin, publisher and consumer personas\n", + "- Execute [notebook 02](02-creating-registry-workflow.ipynb) to create registry as Admin\n", + "\n", + "\n", + "### Publisher Workflow\n", + "![Publisher Workflow](images/publisher_flow_architecture.png)\n", + "\n", + "### Publisher API References\n", + "\n", + "| # | API | Description |\n", + "|---|-----|-------------|\n", + "| 1 | [ListRegistries](https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-agentcore-control/client/list_registries.html) | Discover available registries to publish into |\n", + "| 2 | [GetRegistry](https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-agentcore-control/client/get_registry.html) | Get registry details (name, status, approval config) |\n", + "| 3 | [CreateRegistryRecord](https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-agentcore-control/client/create_registry_record.html) | Create a new MCP or A2A record (CREATING → DRAFT) |\n", + "| 4 | [ListRegistryRecords](https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-agentcore-control/client/list_registry_records.html) | List records, optionally filtered by status |\n", + "| 5 | [GetRegistryRecord](https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-agentcore-control/client/get_registry_record.html) | Get full record details including descriptors |\n", + "| 6 | [UpdateRegistryRecord](https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-agentcore-control/client/update_registry_record.html) | Update a DRAFT record (PATCH with `optionalValue` wrappers) |\n", + "| 7 | [SubmitRegistryRecordForApproval](https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-agentcore-control/client/submit_registry_record_for_approval.html) | Move a record from DRAFT → PENDING_APPROVAL |\n", + "| 8 | [DeleteRegistryRecord](https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-agentcore-control/client/delete_registry_record.html) | Delete a record (publisher can delete their own) |\n", + "\n", + "### Notebook Chain\n", + "\n", + "02 (create registry) → **03 (this notebook)** → 04 (admin approval) → 05 (semantic search)\n", + "\n", + "#### Use Case: Enterprise Payment Processing\n", + "**Publisher Persona:** AnyCompany's payment services team has built a Payment Processing MCP Server, a Loan Processing A2A Agent and other tools. They can submit all records to the agent registry for admin approval, ensuring that payment, loan and other capabilities undergo review before becoming discoverable by other agents across the enterprise." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 1. Install boto3 SDK and dependencies" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Install core dependencies (`boto3` and `python-dotenv`)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install boto3 python-dotenv --force-reinstall" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Initialize boto3 session assuming Publisher Role" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Assume the `publisher_persona` IAM role and create a boto3 session with temporary credentials. All subsequent API calls use this session." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import boto3\n", + "import json\n", + "import time\n", + "import utils\n", + "import os\n", + "from botocore.exceptions import ClientError\n", + "\n", + "AWS_REGION = os.environ.get(\"AWS_DEFAULT_REGION\", \"us-west-2\")\n", + "\n", + "# Auto-detect account ID from current credentials\n", + "sts = boto3.client(\"sts\", region_name=AWS_REGION)\n", + "ACCOUNT_ID = sts.get_caller_identity()[\"Account\"]\n", + "CALLER_ARN = sts.get_caller_identity()[\"Arn\"]\n", + "\n", + "PUBLISHER_ROLE_ARN = f\"arn:aws:iam::{ACCOUNT_ID}:role/publisher_persona\"\n", + "\n", + "print(f\"Account: {PUBLISHER_ROLE_ARN}\")\n", + "\n", + "# Assume the Admin role\n", + "creds = utils.assume_role(\n", + " role_arn=PUBLISHER_ROLE_ARN,\n", + " session_name=\"publisher_session\",\n", + ")\n", + "\n", + "publisher_session = boto3.Session(\n", + " aws_access_key_id=creds[\"AccessKeyId\"],\n", + " aws_secret_access_key=creds[\"SecretAccessKey\"],\n", + " aws_session_token=creds[\"SessionToken\"],\n", + " region_name=AWS_REGION,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Initialize the Control Plane Client\n", + "\n", + "The control plane (`bedrock-agentcore-control`) handles CRUD operations for registries and records." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Control plane client (admin operations)\n", + "cp_client = publisher_session.client(\"bedrock-agentcore-control\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 4. Publisher Lists Registries\n", + "\n", + "Discover available registries to publish records into." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "registries = cp_client.list_registries()\n", + "print(f\"Publisher can see {len(registries.get('registries', []))} registries:\\n\")\n", + "for reg in registries.get(\"registries\", []):\n", + " utils.pp(reg)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Select a Registry" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Find an existing READY registry to work with. If you set REGISTRY_ID, we validate it. Otherwise, we pick the first READY registry from list_registries.\n", + "\n", + "If no READY registry is found, you need to run notebook 02 first to create one." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "REGISTRY_ID = \"\" # Populate this placeholder in case you want to manually pick from above list\n", + "\n", + "## if REGISTRY_ID is left empty, we pick the first READY registry from list_registries\n", + "registry_details = utils.get_or_select_registry(cp_client,REGISTRY_ID,AWS_REGION) \n", + "REGISTRY_ID = registry_details[0]\n", + "REGISTRY_ARN = registry_details[1]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "## 6. Create an MCP Record\n", + "\n", + "Create an MCP registry record with server and tool descriptors. The record starts in `CREATING` and transitions to `DRAFT` once ready.\n", + "\n", + "> This is sample data for demonstration purposes." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 6.1 Define MCP Descriptor Schemas\n", + "\n", + "Define the MCP server metadata and tool input schemas. These describe the payment processing capabilities that consumers will discover." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "mcp_server_schema = json.dumps({\n", + " \"name\": \"io.novacorp/payment-processing-server\",\n", + " \"description\": \"A payment processing MCP server for handling transactions, refunds, and payment status queries\",\n", + " \"version\": \"1.0.0\",\n", + " \"title\": \"Payment Processing Server\",\n", + " \"packages\": [\n", + " {\n", + " \"registryType\": \"npm\",\n", + " \"identifier\": \"@novacorp/payment-processing-mcp\",\n", + " \"version\": \"1.0.0\",\n", + " \"registryBaseUrl\": \"https://registry.npmjs.org\",\n", + " \"runtimeHint\": \"npx\",\n", + " \"transport\": {\"type\": \"stdio\"},\n", + " }\n", + " ],\n", + "})\n", + "\n", + "mcp_tool_schema = json.dumps({\n", + " \"tools\": [\n", + " {\n", + " \"name\": \"process_payment\",\n", + " \"description\": \"Process a new payment transaction for a given amount and currency\",\n", + " \"inputSchema\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"amount\": {\"type\": \"number\", \"description\": \"Payment amount\"},\n", + " \"currency\": {\"type\": \"string\", \"description\": \"ISO 4217 currency code (e.g. USD, EUR)\"},\n", + " \"customer_id\": {\"type\": \"string\", \"description\": \"Unique customer identifier\"},\n", + " \"payment_method\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"Payment method type\",\n", + " \"enum\": [\"credit_card\", \"debit_card\", \"bank_transfer\", \"digital_wallet\"],\n", + " },\n", + " \"description\": {\"type\": \"string\", \"description\": \"Optional payment description\"},\n", + " },\n", + " \"required\": [\"amount\", \"currency\", \"customer_id\", \"payment_method\"],\n", + " },\n", + " },\n", + " {\n", + " \"name\": \"get_payment_status\",\n", + " \"description\": \"Retrieve the current status of a payment by transaction ID\",\n", + " \"inputSchema\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"transaction_id\": {\"type\": \"string\", \"description\": \"Unique transaction identifier\"},\n", + " },\n", + " \"required\": [\"transaction_id\"],\n", + " },\n", + " },\n", + " {\n", + " \"name\": \"process_refund\",\n", + " \"description\": \"Initiate a full or partial refund for a completed transaction\",\n", + " \"inputSchema\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"transaction_id\": {\"type\": \"string\", \"description\": \"Original transaction ID to refund\"},\n", + " \"amount\": {\"type\": \"number\", \"description\": \"Refund amount (omit for full refund)\"},\n", + " \"reason\": {\"type\": \"string\", \"description\": \"Reason for the refund\"},\n", + " },\n", + " \"required\": [\"transaction_id\", \"reason\"],\n", + " },\n", + " },\n", + " ]\n", + "})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 6.2 Create the MCP Registry Record\n", + "\n", + "Submit the MCP record to the registry. \n", + "\n", + "> Note: Running this cell multiple times creates duplicate records with unique IDs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "MCP_RECORD_ID = None\n", + "\n", + "try:\n", + " mcp_resp = cp_client.create_registry_record(\n", + " registryId=REGISTRY_ID,\n", + " name=\"mcp_payment_processing_server\",\n", + " description=\"MCP server for processing payments, refunds, and transaction queries\",\n", + " descriptorType=\"MCP\",\n", + " descriptors={\n", + " \"mcp\": {\n", + " \"server\": {\n", + " \"schemaVersion\": \"2025-12-11\",\n", + " \"inlineContent\": mcp_server_schema,\n", + " },\n", + " \"tools\": {\n", + " \"inlineContent\": mcp_tool_schema,\n", + " },\n", + " }\n", + " },\n", + " recordVersion=\"1.0\",\n", + " )\n", + " MCP_RECORD_ID = mcp_resp[\"recordArn\"].split(\"/\")[-1]\n", + " print(f\"Created MCP record: {MCP_RECORD_ID}\")\n", + " record = utils.wait_for_record_ready(cp_client, REGISTRY_ID, MCP_RECORD_ID)\n", + " print(f\"Status: {record.get('status', 'UNKNOWN')}\")\n", + "\n", + "except ClientError as e:\n", + " if e.response[\"Error\"][\"Code\"] == \"ConflictException\":\n", + " print(\"Record 'payment_processing_server' already exists — looking it up...\")\n", + " records = cp_client.list_registry_records(registryId=REGISTRY_ID)\n", + " for rec in records.get(\"registryRecords\", []):\n", + " if rec[\"name\"] == \"payment_processing_server\":\n", + " MCP_RECORD_ID = rec[\"registryRecordId\"]\n", + " break\n", + " print(f\" Using existing record: {MCP_RECORD_ID}\")\n", + " else:\n", + " raise\n", + "\n", + "print(f\"\\nMCP_RECORD_ID = {MCP_RECORD_ID}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. Create an A2A Record\n", + "\n", + "Create an A2A (Agent-to-Agent) registry record with an agent card descriptor for the Loan Processing Agent.\n", + "\n", + "> Note: Running this cell multiple times creates duplicate records with unique IDs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from botocore.exceptions import ClientError\n", + "\n", + "a2a_agent_card = json.dumps({\n", + " \"protocolVersion\": \"0.3.0\",\n", + " \"name\": \"Loan Processing Agent\",\n", + " \"description\": \"Publishes loan processing capabilities for agent discovery, including loan applications, credit checks, and installment plan support.\",\n", + " \"url\": \"https://example.com/agents/loan\",\n", + " \"version\": \"1.0.0\",\n", + " \"capabilities\": {\"streaming\": True},\n", + " \"defaultInputModes\": [\"text\"],\n", + " \"defaultOutputModes\": [\"text\"],\n", + " \"preferredTransport\": \"JSONRPC\",\n", + " \"skills\": [\n", + " {\n", + " \"id\": \"loan_application_processing\",\n", + " \"name\": \"Loan Application Processing\",\n", + " \"description\": \"Process loan applications and validate required information.\",\n", + " \"tags\": []\n", + " },\n", + " {\n", + " \"id\": \"credit_check_review\",\n", + " \"name\": \"Credit Check Review\",\n", + " \"description\": \"Support credit check workflows and eligibility review.\",\n", + " \"tags\": []\n", + " },\n", + " {\n", + " \"id\": \"installment_plan_management\",\n", + " \"name\": \"Installment Plan Management\",\n", + " \"description\": \"Manage installment plan setup and repayment schedule inquiries.\",\n", + " \"tags\": []\n", + " },\n", + " ],\n", + "})\n", + "\n", + "try:\n", + " a2a_resp = cp_client.create_registry_record(\n", + " registryId=REGISTRY_ID,\n", + " name=\"a2a_loan_agent\",\n", + " description=\"A2A agent for loan processing capabilities including loan applications, credit checks, and installment plan support.\",\n", + " descriptorType=\"A2A\",\n", + " descriptors={\n", + " \"a2a\": {\n", + " \"agentCard\": {\n", + " \"schemaVersion\": \"0.3\", \n", + " \"inlineContent\": a2a_agent_card,\n", + " }\n", + " }\n", + " },\n", + " recordVersion=\"1.0\",\n", + " )\n", + " A2A_RECORD_ID = a2a_resp[\"recordArn\"].split(\"/\")[-1] # recordArn, not registryRecordArn\n", + " print(f\"Created A2A record: {A2A_RECORD_ID}\")\n", + " utils.wait_for_record_ready(cp_client, REGISTRY_ID, A2A_RECORD_ID)\n", + "\n", + "except ClientError as e:\n", + " if e.response[\"Error\"][\"Code\"] == \"ConflictException\":\n", + " print(\"Record 'payment_agent' already exists — looking it up...\")\n", + " records = cp_client.list_registry_records(registryId=REGISTRY_ID)\n", + " for rec in records.get(\"registryRecords\", []):\n", + " if rec[\"name\"] == \"payment_agent\":\n", + " A2A_RECORD_ID = rec[\"recordId\"] # recordId, not registryRecordId\n", + " break\n", + " print(f\" Using existing record: {A2A_RECORD_ID}\")\n", + " else:\n", + " raise\n", + "\n", + "print(f\"\\nA2A_RECORD_ID = {A2A_RECORD_ID}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 8. Create Additional Records\n", + "\n", + "Create 5 more records covering refund analytics, credit scoring, fraud detection, billing disputes, and payment reconciliation. These enrich the registry for search demonstrations in notebook 05.\n", + "\n", + "> Note: Running this cell multiple times creates duplicate records with unique IDs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ADDITIONAL_RECORDS = [\n", + " {\n", + " \"name\": \"mcp_refund_analytics_server\",\n", + " \"description\": \"MCP server for refund trend analysis, chargeback reporting, and refund policy compliance checks.\",\n", + " \"descriptorType\": \"MCP\",\n", + " \"descriptors\": {\n", + " \"mcp\": {\n", + " \"server\": {\n", + " \"schemaVersion\": \"2025-12-11\",\n", + " \"inlineContent\": json.dumps({\n", + " \"name\": \"io.novacorp/refund-analytics-server\",\n", + " \"description\": \"Analyzes refund patterns, chargeback rates, and policy compliance\",\n", + " \"version\": \"1.0.0\",\n", + " }),\n", + " },\n", + " \"tools\": {\n", + " \"inlineContent\": json.dumps({\"tools\": [\n", + " {\"name\": \"get_refund_trends\", \"description\": \"Analyze refund trends over a date range\", \"inputSchema\": {\"type\": \"object\", \"properties\": {\"start_date\": {\"type\": \"string\"}, \"end_date\": {\"type\": \"string\"}}, \"required\": [\"start_date\"]}},\n", + " {\"name\": \"check_chargeback_rate\", \"description\": \"Get chargeback rate for a merchant\", \"inputSchema\": {\"type\": \"object\", \"properties\": {\"merchant_id\": {\"type\": \"string\"}}, \"required\": [\"merchant_id\"]}},\n", + " ]}),\n", + " },\n", + " }\n", + " },\n", + " \"recordVersion\": \"1.0\",\n", + " },\n", + " {\n", + " \"name\": \"a2a_credit_score_agent\",\n", + " \"description\": \"A2A agent for real-time credit score retrieval, credit history analysis, and risk assessment for lending decisions.\",\n", + " \"descriptorType\": \"A2A\",\n", + " \"descriptors\": {\n", + " \"a2a\": {\n", + " \"agentCard\": {\n", + " \"schemaVersion\": \"0.3\",\n", + " \"inlineContent\": json.dumps({\n", + " \"protocolVersion\": \"0.3.0\",\n", + " \"name\": \"Credit Score Agent\",\n", + " \"description\": \"Retrieves credit scores, analyzes credit history, and provides risk assessments for lending workflows.\",\n", + " \"url\": \"https://example.com/agents/credit-score\",\n", + " \"version\": \"1.0.0\",\n", + " \"capabilities\": {\"streaming\": False},\n", + " \"defaultInputModes\": [\"text\"],\n", + " \"defaultOutputModes\": [\"text\"],\n", + " \"skills\": [\n", + " {\"id\": \"get_credit_score\", \"name\": \"Get Credit Score\", \"description\": \"Retrieve current credit score for a customer.\", \"tags\": []},\n", + " {\"id\": \"credit_risk_assessment\", \"name\": \"Credit Risk Assessment\", \"description\": \"Evaluate lending risk based on credit history.\", \"tags\": []},\n", + " ],\n", + " }),\n", + " }\n", + " }\n", + " },\n", + " \"recordVersion\": \"1.0\",\n", + " },\n", + " {\n", + " \"name\": \"mcp_fraud_detection_server\",\n", + " \"description\": \"MCP server for real-time transaction fraud detection, suspicious activity flagging, and fraud case management.\",\n", + " \"descriptorType\": \"MCP\",\n", + " \"descriptors\": {\n", + " \"mcp\": {\n", + " \"server\": {\n", + " \"schemaVersion\": \"2025-12-11\",\n", + " \"inlineContent\": json.dumps({\n", + " \"name\": \"io.novacorp/fraud-detection-server\",\n", + " \"description\": \"Detects fraudulent transactions and manages fraud cases\",\n", + " \"version\": \"1.0.0\",\n", + " }),\n", + " },\n", + " \"tools\": {\n", + " \"inlineContent\": json.dumps({\"tools\": [\n", + " {\"name\": \"scan_transaction\", \"description\": \"Scan a transaction for fraud indicators\", \"inputSchema\": {\"type\": \"object\", \"properties\": {\"transaction_id\": {\"type\": \"string\"}}, \"required\": [\"transaction_id\"]}},\n", + " {\"name\": \"flag_suspicious_activity\", \"description\": \"Flag an account for suspicious activity review\", \"inputSchema\": {\"type\": \"object\", \"properties\": {\"account_id\": {\"type\": \"string\"}, \"reason\": {\"type\": \"string\"}}, \"required\": [\"account_id\", \"reason\"]}},\n", + " ]}),\n", + " },\n", + " }\n", + " },\n", + " \"recordVersion\": \"1.0\",\n", + " },\n", + " {\n", + " \"name\": \"a2a_billing_dispute_agent\",\n", + " \"description\": \"A2A agent for handling billing disputes, charge contestations, and resolution tracking across customer accounts.\",\n", + " \"descriptorType\": \"A2A\",\n", + " \"descriptors\": {\n", + " \"a2a\": {\n", + " \"agentCard\": {\n", + " \"schemaVersion\": \"0.3\",\n", + " \"inlineContent\": json.dumps({\n", + " \"protocolVersion\": \"0.3.0\",\n", + " \"name\": \"Billing Dispute Agent\",\n", + " \"description\": \"Manages billing disputes, charge contestations, and tracks resolution status.\",\n", + " \"url\": \"https://example.com/agents/billing-dispute\",\n", + " \"version\": \"1.0.0\",\n", + " \"capabilities\": {\"streaming\": True},\n", + " \"defaultInputModes\": [\"text\"],\n", + " \"defaultOutputModes\": [\"text\"],\n", + " \"skills\": [\n", + " {\"id\": \"open_dispute\", \"name\": \"Open Dispute\", \"description\": \"Open a new billing dispute for a customer charge.\", \"tags\": []},\n", + " {\"id\": \"track_resolution\", \"name\": \"Track Resolution\", \"description\": \"Check the status of an ongoing billing dispute.\", \"tags\": []},\n", + " ],\n", + " }),\n", + " }\n", + " }\n", + " },\n", + " \"recordVersion\": \"1.0\",\n", + " },\n", + " {\n", + " \"name\": \"mcp_payment_reconciliation_server\",\n", + " \"description\": \"MCP server for reconciling payment records across systems, identifying discrepancies, and generating settlement reports.\",\n", + " \"descriptorType\": \"MCP\",\n", + " \"descriptors\": {\n", + " \"mcp\": {\n", + " \"server\": {\n", + " \"schemaVersion\": \"2025-12-11\",\n", + " \"inlineContent\": json.dumps({\n", + " \"name\": \"io.novacorp/payment-reconciliation-server\",\n", + " \"description\": \"Reconciles payments across systems and generates settlement reports\",\n", + " \"version\": \"1.0.0\",\n", + " }),\n", + " },\n", + " \"tools\": {\n", + " \"inlineContent\": json.dumps({\"tools\": [\n", + " {\"name\": \"reconcile_payments\", \"description\": \"Reconcile payment records between two systems for a date range\", \"inputSchema\": {\"type\": \"object\", \"properties\": {\"source_system\": {\"type\": \"string\"}, \"target_system\": {\"type\": \"string\"}, \"date\": {\"type\": \"string\"}}, \"required\": [\"source_system\", \"target_system\", \"date\"]}},\n", + " {\"name\": \"generate_settlement_report\", \"description\": \"Generate a settlement report for a merchant\", \"inputSchema\": {\"type\": \"object\", \"properties\": {\"merchant_id\": {\"type\": \"string\"}, \"period\": {\"type\": \"string\"}}, \"required\": [\"merchant_id\", \"period\"]}},\n", + " ]}),\n", + " },\n", + " }\n", + " },\n", + " \"recordVersion\": \"1.0\",\n", + " },\n", + " {\n", + " \"name\": \"custom_payment_gateway_config\",\n", + " \"description\": \"Custom descriptor for AnyCompany's payment gateway configuration, including supported providers, retry policies, and regional routing rules.\",\n", + " \"descriptorType\": \"CUSTOM\",\n", + " \"descriptors\": {\n", + " \"custom\": {\n", + " \"inlineContent\": json.dumps({\n", + " \"gatewayName\": \"NovaCorp Payment Gateway\",\n", + " \"version\": \"2.1.0\",\n", + " \"supportedProviders\": [\"stripe\", \"adyen\", \"square\"],\n", + " \"endpoint\": \"https://gateway.novacorp.example.com/v2\",\n", + " \"retryPolicy\": {\"maxRetries\": 3, \"backoffMs\": 500},\n", + " \"regionalRouting\": {\n", + " \"us\": \"us-east-1\",\n", + " \"eu\": \"eu-west-1\",\n", + " \"apac\": \"ap-southeast-1\"\n", + " },\n", + " }),\n", + " }\n", + " },\n", + " \"recordVersion\": \"1.0\",\n", + " },\n", + "]\n", + "\n", + "additional_record_ids = []\n", + "for rec in ADDITIONAL_RECORDS:\n", + " try:\n", + " resp = cp_client.create_registry_record(registryId=REGISTRY_ID, **rec)\n", + " rid = resp[\"recordArn\"].split(\"/\")[-1]\n", + " additional_record_ids.append(rid)\n", + " print(f\" ✅ Created [{rec['descriptorType']}] {rec['name']} — {rid}\")\n", + " utils.wait_for_record_ready(cp_client, REGISTRY_ID, rid)\n", + " except ClientError as e:\n", + " if e.response[\"Error\"][\"Code\"] == \"ConflictException\":\n", + " print(f\" ⚠️ {rec['name']} already exists — skipping.\")\n", + " else:\n", + " print(f\" ❌ Failed: {rec['name']} — {e}\")\n", + "\n", + "print(f\"\\nCreated {len(additional_record_ids)} additional records.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "## 9. List Records in the Registry\n", + "\n", + "List all records in the registry. Filter by status or descriptor type to narrow results." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# List all records\n", + "\n", + "records_resp = cp_client.list_registry_records(\n", + " registryId=REGISTRY_ID\n", + ")\n", + "\n", + "print(f\"Total records in registry: {len(records_resp.get('registryRecords', []))}\\n\")\n", + "for rec in records_resp.get(\"registryRecords\", []):\n", + "\n", + " print(f\" {rec['name']} | {rec['recordId']} | {rec['descriptorType']} | {rec['status']}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "## 10. Get Record Details\n", + "\n", + "Retrieve full details for a specific record, including its descriptors." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get a single MCP record detail\n", + "mcp_detail = cp_client.get_registry_record(\n", + " registryId=REGISTRY_ID,\n", + " recordId=MCP_RECORD_ID,\n", + ")\n", + "\n", + "print(\"MCP Record Details:\")\n", + "utils.pp(mcp_detail)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Retrieve the A2A record details, including the agent card descriptor." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get a single A2A record detail\n", + "a2a_detail = cp_client.get_registry_record(\n", + " registryId=REGISTRY_ID,\n", + " recordId=A2A_RECORD_ID,\n", + ")\n", + "\n", + "print(\"A2A Record Details:\")\n", + "utils.pp(a2a_detail)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "## 11. Update a Record\n", + "\n", + "Records can be updated while in `DRAFT` status. The `UpdateRegistryRecord` API uses PATCH semantics with `optionalValue` wrappers.\n", + "\n", + "Here we update the MCP tool schema to add a `list_transactions` tool." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "updated_tool_schema = json.dumps({\n", + " \"tools\": [\n", + " {\n", + " \"name\": \"list_transactions\",\n", + " \"description\": \"List payment transactions for a customer with optional date filtering\",\n", + " \"inputSchema\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"customer_id\": {\"type\": \"string\", \"description\": \"Unique customer identifier\"},\n", + " \"start_date\": {\"type\": \"string\", \"description\": \"Start date filter (ISO 8601)\"},\n", + " \"end_date\": {\"type\": \"string\", \"description\": \"End date filter (ISO 8601)\"},\n", + " \"status\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"Filter by transaction status\",\n", + " \"enum\": [\"pending\", \"completed\", \"failed\", \"refunded\"],\n", + " },\n", + " },\n", + " \"required\": [\"customer_id\"],\n", + " },\n", + " }\n", + " ]\n", + "})\n", + "\n", + "update_resp = cp_client.update_registry_record(\n", + " registryId=REGISTRY_ID,\n", + " recordId=MCP_RECORD_ID,\n", + " descriptors={\n", + " \"optionalValue\": {\n", + " \"mcp\": {\n", + " \"optionalValue\": {\n", + " \"tools\": {\n", + " \"optionalValue\": {\n", + " \"inlineContent\": updated_tool_schema,\n", + " }\n", + " }\n", + " }\n", + " }\n", + " }\n", + " },\n", + ")\n", + "\n", + "print(\"Update submitted — waiting for record to settle...\")\n", + "updated_record = utils.wait_for_record_ready(cp_client, REGISTRY_ID, MCP_RECORD_ID)\n", + "print(\"\\nUpdated record:\")\n", + "utils.pp(updated_record)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "## 12. Publisher Submits All Records in \"DRAFT\" status for Admin Approval \n", + "\n", + "Use `SubmitRegistryRecordForApproval` to transition a record from `DRAFT` → `PENDING_APPROVAL`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Submit all DRAFT records for approval (MCP + A2A)\n", + "draft_resp = cp_client.list_registry_records(\n", + " registryId=REGISTRY_ID,\n", + " status=\"DRAFT\",\n", + ")\n", + "draft_records = draft_resp.get(\"registryRecords\", [])\n", + "print(f\"Found {len(draft_records)} DRAFT records\\n\")\n", + "\n", + "for rec in draft_records:\n", + " record_id = rec[\"recordId\"]\n", + " try:\n", + " submit_resp = cp_client.submit_registry_record_for_approval(\n", + " registryId=REGISTRY_ID,\n", + " recordId=record_id,\n", + " )\n", + " print(f\"Submitted: {rec['name']} ({rec['descriptorType']}) — {record_id}\")\n", + " utils.wait_for_record_ready(cp_client, REGISTRY_ID, record_id)\n", + " except ClientError as e:\n", + " error_code = e.response[\"Error\"][\"Code\"]\n", + " error_msg = e.response[\"Error\"].get(\"Message\", \"\")\n", + " print(f\"Failed: {rec['name']} ({record_id}) — {error_code}: {error_msg}\")\n", + " print()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Verify Pending Records\n", + "\n", + "Confirm that all submitted records are now in `PENDING_APPROVAL` status." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Verify all records that are in PENDING_APPROVAL\n", + "\n", + "pending_resp = cp_client.list_registry_records(\n", + " registryId=REGISTRY_ID,\n", + " status=\"PENDING_APPROVAL\",\n", + ")\n", + "pending_records = pending_resp.get(\"registryRecords\", [])\n", + "print(f\"Records in PENDING_APPROVAL: {len(pending_records)}\\n\")\n", + "\n", + "for rec in pending_records:\n", + " print(f\" {rec['name']} ({rec['descriptorType']}) — {rec['recordId']}: {rec['status']}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "## 13. Cleanup\n", + "\n", + "Remove demo resources created in this notebook.\n", + "\n", + "⚠️ Uncomment the code below to proceed with deletion." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# # Delete records (publisher can delete their own records)\n", + "# for rid, label in [(MCP_RECORD_ID, \"MCP\"), (A2A_RECORD_ID, \"A2A\")]:\n", + "# try:\n", + "# cp_client.delete_registry_record(\n", + "# registryId=REGISTRY_ID, recordId=rid\n", + "# )\n", + "# print(f\"Deleted {label} record: {rid}\")\n", + "# except Exception as e:\n", + "# print(f\"Record cleanup ({label}): {e}\")\n", + "\n", + "# print(\"\\nCleanup complete!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Delete Specific Records by ID\n", + "\n", + "Use this cell to delete specific records by their record IDs. Update `IDS_TO_DELETE` with the IDs you want to remove." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# IDS_TO_DELETE = [\"EtW5lsG3TFBk\"] # Replace with your record IDs\n", + "\n", + "# # Fetch current records from the registry\n", + "# all_records = cp_client.list_registry_records(registryId=REGISTRY_ID).get(\"registryRecords\", [])\n", + "\n", + "# targets = [r for r in all_records if r.get(\"recordId\") in IDS_TO_DELETE]\n", + "# print(f\"{len(targets)} record(s) matched\\n\")\n", + "# for r in targets:\n", + "# print(f\" {r.get('name')} — {r.get('recordId')}\")\n", + "\n", + "# # Delete matched records\n", + "# for r in targets:\n", + "# try:\n", + "# cp_client.delete_registry_record(registryId=REGISTRY_ID, recordId=r[\"recordId\"])\n", + "# print(f\" Deleted: {r['name']} ({r['recordId']})\")\n", + "# except Exception as e:\n", + "# print(f\" FAILED: {r['name']} ({r['recordId']}) — {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Pre-requisites\n", + "- **Notebook 01** — [Create User Personas](01-create-user-personas-workflow.ipynb): Set up user personas: admin, publisher, consumer\n", + "- **Notebook 02** — [Creating Registry](02-creating-registry-workflow.ipynb): Admin creates a registry\n", + "\n", + "## Next Steps\n", + "- **Notebook 04** — [Admin Approval](04-admin-approval-workflow.ipynb): Admin Approval workflow \n", + "- **Notebook 05** — [Semantic Search](05-search-registry-workflow.ipynb): Search approved records using NLQ as a Consumer" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "finalchangeenv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/04-admin-approval-workflow.ipynb b/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/04-admin-approval-workflow.ipynb new file mode 100644 index 000000000..26ebec02f --- /dev/null +++ b/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/04-admin-approval-workflow.ipynb @@ -0,0 +1,805 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 04 - Approval Workflows for Registry as Admin\n", + "\n", + "This notebook demonstrates how to **approve and reject** Registry records using two approaches:\n", + "\n", + "1. **Manual approval** — Using the AWS Agent Registry SDK to list pending records, inspect them, and approve or reject with a reason\n", + "2. **Automated approval** — Using the registry's built-in `autoApproval` flag to automatically approve new submissions\n", + "\n", + "## What You'll Learn\n", + "\n", + "- Authenticate as the **Admin** persona and discover an existing registry\n", + "- List records pending approval and inspect their details\n", + "- **Approve** or **reject** records via the SDK with status reasons\n", + "- Create test records for the automation demo\n", + "- Enable the **autoApproval** flag on a registry to automatically approve new submissions\n", + "- Run an **end-to-end test** of the autoApproval workflow\n", + "- Compare manual vs autoApproval approaches and best practices\n", + "\n", + "## Prerequisites\n", + "\n", + "- boto3 >= 1.42.87\n", + "- Execute [notebook 01](01-create-user-personas-workflow.ipynb) to create IAM roles for admin, publisher and consumer personas\n", + "- Execute [notebook 02](02-creating-registry-workflow.ipynb) to create registry as Admin\n", + "- Execute [notebook 03](03-publishing-records-workflow.ipynb) to publish records to registry as Publisher\n", + "\n", + "## Admin Approval Workflow\n", + "![Admin Approval Workflow](images/admin_approval_flow_architecture.png)\n", + "\n", + "## Admin Approval API References\n", + "\n", + "| # | API | Description |\n", + "|---|-----|-------------|\n", + "| 1 | [ListRegistryRecords](https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-agentcore-control/client/list_registry_records.html) | List all records in a registry and filter by status |\n", + "| 2 | [GetRegistryRecord](https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-agentcore-control/client/get_registry_record.html) | Inspect record details and verify status changes |\n", + "| 3 | [UpdateRegistryRecordStatus](https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-agentcore-control/client/update_registry_record_status.html) | Approve or reject a pending record with a status reason |\n", + "| 4 | [CreateRegistryRecord](https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-agentcore-control/client/create_registry_record.html) | Create a test record for the approval/rejection demo |\n", + "| 5 | [SubmitRegistryRecordForApproval](https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-agentcore-control/client/submit_registry_record_for_approval.html) | Submit a draft record for admin review |\n", + "| 6 | [GetRegistry](https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-agentcore-control/client/get_registry.html) | Check current registry configuration (autoApproval flag) |\n", + "| 7 | [UpdateRegistry](https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-agentcore-control/client/update_registry.html) | Toggle the autoApproval setting on a registry |\n", + "| 8 | [DeleteRegistryRecord](https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-agentcore-control/client/delete_registry_record.html) | Clean up test records after the demo |\n", + "\n", + "\n", + "### Notebook Chain\n", + "\n", + "02 (create registry) → 03 (publish records) → **04 (this notebook)** → 05 (semantic search)\n", + "\n", + "#### Use Case: Enterprise Payment Processing \n", + "**Admin Persona:** AnyCompany registry administrator reviews submitted records through a human-in-the-loop approval process, where they evaluate each submission against security, compliance, and quality standards before deciding to approve or reject the record. Upon approval, the validated capabilities are published to the registry and become discoverable to authorized AI agents across the enterprise, while rejected submissions are returned to development teams with feedback for remediation." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 1. Install boto3 SDK and dependencies" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Install core dependencies (`boto3` and `python-dotenv`)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install boto3 python-dotenv --force-reinstall" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Initialize boto3 Session as Admin\n", + "\n", + "Assume the `admin_persona` IAM role and create a boto3 session with temporary credentials." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import boto3\n", + "import json\n", + "import time\n", + "import os\n", + "import sys\n", + "import botocore.exceptions\n", + "import utils\n", + "\n", + "AWS_REGION = os.environ.get(\"AWS_DEFAULT_REGION\", \"us-west-2\")\n", + "\n", + "# Auto-detect account ID from current credentials\n", + "sts = boto3.client(\"sts\", region_name=AWS_REGION)\n", + "ACCOUNT_ID = sts.get_caller_identity()[\"Account\"]\n", + "CALLER_ARN = sts.get_caller_identity()[\"Arn\"]\n", + "\n", + "ADMIN_ROLE_ARN = f\"arn:aws:iam::{ACCOUNT_ID}:role/admin_persona\"\n", + "\n", + "print(f\"Account: {ADMIN_ROLE_ARN}\")\n", + "\n", + "# Assume the Admin role\n", + "creds = utils.assume_role(\n", + " role_arn=ADMIN_ROLE_ARN,\n", + " session_name=\"admin-session\",\n", + ")\n", + "\n", + "admin_session = boto3.Session(\n", + " aws_access_key_id=creds[\"AccessKeyId\"],\n", + " aws_secret_access_key=creds[\"SecretAccessKey\"],\n", + " aws_session_token=creds[\"SessionToken\"],\n", + " region_name=AWS_REGION,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Initialize the Control Plane Client\n", + "\n", + "The control plane (`bedrock-agentcore-control`) handles CRUD operations for registries and records." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Control plane client (admin operations)\n", + "cp_client = admin_session.client(\"bedrock-agentcore-control\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 4. Select a Registry\n", + "\n", + "Find an existing READY registry to work with. If you set `REGISTRY_ID` above, we validate it.\n", + "Otherwise, we pick the first READY registry from `list_registries`.\n", + "\n", + "If no READY registry is found, you need to run **notebook 02** first to create one." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "REGISTRY_ID = \"\" # Populate this placeholder in case you want to manually pick from above list\n", + "\n", + "## if REGISTRY_ID is left empty, we pick the first READY registry from list_registries\n", + "registry_details = utils.get_or_select_registry(cp_client,REGISTRY_ID,AWS_REGION) \n", + "REGISTRY_ID = registry_details[0]\n", + "REGISTRY_ARN = registry_details[1]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 5. List Existing Records\n", + "\n", + "List all records currently in the registry, including any records published from notebook 03.\n", + "This gives us a baseline view before creating test records or performing approvals." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " all_records = utils.list_records_with_ids(cp_client, REGISTRY_ID)\n", + "\n", + " print(f\"Total records in registry: {len(all_records)}\\n\")\n", + " for rec in all_records:\n", + " status_marker = \"🟡\" if rec[\"status\"] == \"PENDING_APPROVAL\" else \"•\"\n", + " print(f\" {status_marker} [{rec['status']}] {rec['name']} ({rec['descriptorType']}) — {rec['recordId']}\")\n", + "\n", + " pending = utils.filter_pending_records(all_records)\n", + " print(f\"\\n📋 Records pending approval: {len(pending)}\")\n", + " for rec in pending:\n", + " print(f\" • {rec['name']} | {rec['descriptorType']} | {rec['recordId']}\")\n", + "\n", + " if not pending:\n", + " print(\"\\n No records pending approval right now.\")\n", + "\n", + "except botocore.exceptions.ClientError as e:\n", + " error_code = e.response[\"Error\"][\"Code\"]\n", + " if error_code == \"ConflictException\":\n", + " print(f\"❌ Registry {REGISTRY_ID} is not in READY state.\")\n", + " print(\" Wait for the registry to finish provisioning or check its status.\")\n", + " else:\n", + " print(f\"❌ Error listing records: {error_code} — {e}\")\n", + " raise" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you ran notebook 03, you should see records in `PENDING_APPROVAL` status above." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 6. Manual Approval Workflow\n", + "\n", + "In this section we demonstrate the manual admin approval process:\n", + "1. Approve records from notebook 03 (so they're searchable in notebook 05)\n", + "2. Create a test record and show reject record flow" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 6a. Approve Records from Notebook 03\n", + "\n", + "If you ran notebook 03, those records are waiting for approval. Let's approve them\n", + "so they're searchable in notebook 05. The record transitions from `PENDING_APPROVAL` → `APPROVED` and becomes searchable by consumers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " all_records = utils.list_records_with_ids(cp_client, REGISTRY_ID)\n", + " pending = utils.filter_pending_records(all_records)\n", + "\n", + " # Filter out our test records (admin_approval_test_*)\n", + " records_from_103 = [r for r in pending]\n", + "\n", + " if records_from_103:\n", + " print(f\"Found {len(records_from_103)} pending record(s) from notebook 03:\\n\")\n", + " for rec in records_from_103:\n", + " record_id = rec[\"recordId\"]\n", + " print(f\" Approving: {rec['name']} ({record_id})...\")\n", + " try:\n", + " cp_client.update_registry_record_status(\n", + " registryId=REGISTRY_ID,\n", + " recordId=record_id,\n", + " status=\"APPROVED\",\n", + " statusReason=\"Approved by admin in notebook 04 — record from notebook 03.\",\n", + " )\n", + " verified = cp_client.get_registry_record(\n", + " registryId=REGISTRY_ID, recordId=record_id\n", + " )\n", + " print(f\" ✅ Status: {verified['status']}\")\n", + " except botocore.exceptions.ClientError as e:\n", + " error_code = e.response[\"Error\"][\"Code\"]\n", + " if error_code == \"ConflictException\":\n", + " print(f\" ⚠️ Record not in PENDING_APPROVAL state — may already be approved.\")\n", + " else:\n", + " print(f\" ❌ Error: {error_code} — {e}\")\n", + " raise\n", + " else:\n", + " print(\"No records from notebook 03 found\")\n", + "\n", + "except botocore.exceptions.ClientError as e:\n", + " error_code = e.response[\"Error\"][\"Code\"]\n", + " print(f\"❌ Error listing records: {error_code} — {e}\")\n", + " raise" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 6b. Reject a Test Record\n", + "\n", + "First, create a test record specifically for demonstrating the rejection flow, then submit it for approval." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create a test record for rejection\n", + "try:\n", + " reject_test_resp = cp_client.create_registry_record(\n", + " registryId=REGISTRY_ID,\n", + " name=\"admin_approval_test_reject\",\n", + " description=\"Test record created to demonstrate the admin rejection workflow.\",\n", + " descriptorType=\"CUSTOM\",\n", + " descriptors={\n", + " \"custom\": {\n", + " \"inlineContent\": json.dumps({\n", + " \"type\": \"test-record\",\n", + " \"purpose\": \"rejection-demo\",\n", + " \"note\": \"This record intentionally has minimal metadata to demonstrate rejection.\"\n", + " })\n", + " }\n", + " },\n", + " recordVersion=\"0.1\",\n", + " )\n", + " REJECT_RECORD_ID = reject_test_resp[\"recordArn\"].split(\"/\")[-1]\n", + " print(f\"Created test record: {REJECT_RECORD_ID}\")\n", + " utils.wait_for_record_ready(cp_client, REGISTRY_ID, REJECT_RECORD_ID)\n", + "\n", + " # Submit for approval so it can be rejected\n", + " cp_client.submit_registry_record_for_approval(\n", + " registryId=REGISTRY_ID, recordId=REJECT_RECORD_ID\n", + " )\n", + " utils.wait_for_record_ready(cp_client, REGISTRY_ID, REJECT_RECORD_ID)\n", + " print(f\"Submitted for approval: {REJECT_RECORD_ID}\")\n", + "\n", + "except botocore.exceptions.ClientError as e:\n", + " print(f\"Error creating test record: {e}\")\n", + " raise" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now reject the test record to demonstrate the rejection flow.\n", + "The publisher can see the rejection reason and update the record accordingly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if REJECT_RECORD_ID:\n", + " try:\n", + " reject_resp = cp_client.update_registry_record_status(\n", + " registryId=REGISTRY_ID,\n", + " recordId=REJECT_RECORD_ID,\n", + " status=\"REJECTED\",\n", + " statusReason=\"Record rejected — missing detailed tool descriptions. Please update and resubmit.\",\n", + " )\n", + " print(f\"❌ Record {REJECT_RECORD_ID} rejected.\")\n", + "\n", + " # Verify the status change\n", + " verified = cp_client.get_registry_record(\n", + " registryId=REGISTRY_ID, recordId=REJECT_RECORD_ID\n", + " )\n", + " print(f\" Verified status: {verified['status']}\")\n", + " print(f\" Status reason: {verified.get('statusReason', 'N/A')}\")\n", + "\n", + " except botocore.exceptions.ClientError as e:\n", + " error_code = e.response[\"Error\"][\"Code\"]\n", + " if error_code == \"ConflictException\":\n", + " print(f\"⚠️ Record {REJECT_RECORD_ID} is not in PENDING_APPROVAL state.\")\n", + " print(\" It may have already been approved or rejected.\")\n", + " elif error_code == \"ValidationException\":\n", + " print(f\"⚠️ Record {REJECT_RECORD_ID} is not in PENDING_APPROVAL state.\")\n", + " print(\" Make sure you ran Section 6 (submit for approval) first.\")\n", + " else:\n", + " print(f\"❌ Error rejecting record: {error_code} — {e}\")\n", + " raise\n", + "else:\n", + " print(\"No reject test record found. Run Section 6 first.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### List All Records\n", + "\n", + "View all records in the registry after the approval and rejection operations above." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "all_records = utils.list_records_with_ids(cp_client, REGISTRY_ID)\n", + "\n", + "print(f\"Total records in registry: {len(all_records)}\\n\")\n", + "for rec in all_records:\n", + " status_marker = \"🟡\" if rec[\"status\"] == \"PENDING_APPROVAL\" else \"•\"\n", + " print(f\" {status_marker} [{rec['status']}] {rec['name']} ({rec['descriptorType']}) — {rec['recordId']}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 7. Automated Approval (autoApproval Flag)\n", + "\n", + "AWS Agent Registry provides a built-in **autoApproval** flag. When enabled, any new record submitted for approval is\n", + "automatically approved without manual intervention.\n", + "\n", + "**Recommendation:** For production workloads, it is recommended for a thorough review of records before making them discoverable via the Registry.\n", + "\n", + "The workflow:\n", + "1. Show current registry configuration (`autoApproval` should be `False` from notebook 02)\n", + "2. Update the registry to set `autoApproval: True`\n", + "3. Create a new test record and submit it for approval\n", + "4. List registry records to confirm it was automatically approved\n", + "5. Restore the registry back to `autoApproval: False`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 7a. Show Current Registry Configuration\n", + "\n", + "Verify the current `autoApproval` setting before making changes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " registry_config = cp_client.get_registry(registryId=REGISTRY_ID)\n", + " current_auto_approval = registry_config.get(\"approvalConfiguration\", {}).get(\"autoApproval\", False)\n", + "\n", + " print(f\"Registry: {registry_config.get('name', 'N/A')} ({REGISTRY_ID})\")\n", + " print(f\"Status: {registry_config.get('status', 'N/A')}\")\n", + " print(f\"\\nCurrent approvalConfiguration:\")\n", + " print(f\" autoApproval: {current_auto_approval}\")\n", + "\n", + " if current_auto_approval:\n", + " print(\"\\n⚠️ autoApproval is already True. Records are being auto-approved.\")\n", + " print(\" We will still demonstrate the workflow below.\")\n", + " else:\n", + " print(\"\\n✅ autoApproval is False — manual approval is required.\")\n", + " print(\" We will enable it in the next step.\")\n", + "\n", + "except botocore.exceptions.ClientError as e:\n", + " error_code = e.response[\"Error\"][\"Code\"]\n", + " print(f\"❌ Error getting registry config: {error_code} — {e}\")\n", + " raise" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 7b. Enable autoApproval\n", + "\n", + "Update the registry to set `autoApproval: True`. After this change, any new record\n", + "submitted for approval will be automatically approved by the service." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " update_resp = cp_client.update_registry(\n", + " registryId=REGISTRY_ID,\n", + " approvalConfiguration={\"optionalValue\": {\"autoApproval\": True}}\n", + " )\n", + "\n", + " print(f\"✅ Registry updated — autoApproval is now enabled.\")\n", + " print(f\" Updated at: {update_resp.get('updatedAt', 'N/A')}\")\n", + "\n", + " # Verify the change\n", + " verify_config = cp_client.get_registry(registryId=REGISTRY_ID)\n", + " verified_auto = verify_config.get(\"approvalConfiguration\", {}).get(\"autoApproval\", False)\n", + " print(f\" Verified autoApproval: {verified_auto}\")\n", + "\n", + " while True:\n", + " r = cp_client.get_registry(registryId=REGISTRY_ID)\n", + " if r[\"status\"] == \"READY\":\n", + " print(f\"Registry is READY\")\n", + " break\n", + " print(f\"Status: {r['status']} - waiting...\")\n", + " time.sleep(3)\n", + "\n", + "except botocore.exceptions.ClientError as e:\n", + " error_code = e.response[\"Error\"][\"Code\"]\n", + " if error_code == \"ConflictException\":\n", + " print(f\"❌ Registry {REGISTRY_ID} is not in READY state.\")\n", + " print(\" The registry must be in READY state before it can be updated.\")\n", + " print(\" Wait for the registry to finish provisioning and re-run this cell.\")\n", + " elif error_code == \"AccessDeniedException\":\n", + " print(f\"❌ Access denied: {e}\")\n", + " print(\" Verify admin_persona has bedrock-agentcore:UpdateRegistry permission.\")\n", + " raise\n", + " else:\n", + " print(f\"❌ Error updating registry: {error_code} — {e}\")\n", + " raise" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 7c. Create and Submit a Test Record (autoApproval Enabled)\n", + "\n", + "Create a new test A2A record and submit it for approval. With `autoApproval: True`,\n", + "the record should be automatically approved without any manual intervention." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Adding a record in a registry with auto-approval enabled" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from botocore.exceptions import ClientError\n", + "\n", + "a2a_agent_card = json.dumps({\n", + " \"protocolVersion\": \"0.3.0\",\n", + " \"name\": \"Refund Processing Agent\",\n", + " \"description\": \"Handles payment refunds\",\n", + " \"url\": \"https://example.com/agents/refund\",\n", + " \"version\": \"1.0.0\",\n", + " \"capabilities\": {\"streaming\": True}, \n", + " \"defaultInputModes\": [\"text\"], \n", + " \"defaultOutputModes\": [\"text\"], \n", + " \"preferredTransport\": \"JSONRPC\", \n", + " \"skills\": [\n", + " {\"id\": \"refund_handling\", \"name\": \"Refund Handling\", \"description\": \"Handle refunds\", \"tags\": []}\n", + " ],\n", + "})\n", + "\n", + "\n", + "try:\n", + " a2a_resp = cp_client.create_registry_record(\n", + " registryId=REGISTRY_ID,\n", + " name=\"a2a_refund_agent_auto_approval\",\n", + " description=\"A2A agent for refunds\",\n", + " descriptorType=\"A2A\",\n", + " descriptors={\n", + " \"a2a\": {\n", + " \"agentCard\": {\n", + " \"schemaVersion\": \"0.3\", \n", + " \"inlineContent\": a2a_agent_card,\n", + " }\n", + " }\n", + " },\n", + " recordVersion=\"1.0\",\n", + " )\n", + " A2A_RECORD_ID = a2a_resp[\"recordArn\"].split(\"/\")[-1] # recordArn, not registryRecordArn\n", + " print(f\"Created A2A record: {A2A_RECORD_ID}\")\n", + " utils.wait_for_record_ready(cp_client, REGISTRY_ID, A2A_RECORD_ID)\n", + "\n", + "except ClientError as e:\n", + " if e.response[\"Error\"][\"Code\"] == \"ConflictException\":\n", + " records = cp_client.list_registry_records(registryId=REGISTRY_ID)\n", + " for rec in records.get(\"registryRecords\", []):\n", + " if rec[\"name\"] == \"payment_agent\":\n", + " A2A_RECORD_ID = rec[\"recordId\"] # recordId, not registryRecordId\n", + " break\n", + " print(f\" Using existing record: {A2A_RECORD_ID}\")\n", + " else:\n", + " raise\n", + "\n", + "print(f\"\\nA2A_RECORD_ID = {A2A_RECORD_ID}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Submitting the record for auto-approval" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Wait for record to be ready, then submit for approval\n", + "try:\n", + " print(f\"Waiting for record {A2A_RECORD_ID} to be ready...\")\n", + " ready_resp = utils.wait_for_record_ready(cp_client, REGISTRY_ID, A2A_RECORD_ID)\n", + "\n", + " if ready_resp[\"status\"] == \"DRAFT\":\n", + " cp_client.submit_registry_record_for_approval(\n", + " registryId=REGISTRY_ID, recordId=A2A_RECORD_ID\n", + " )\n", + " print(f\"✅ Record submitted for approval — autoApproval should handle it automatically.\")\n", + " elif ready_resp[\"status\"] == \"PENDING_APPROVAL\":\n", + " print(\"Record is already PENDING_APPROVAL.\")\n", + " elif ready_resp[\"status\"] == \"APPROVED\":\n", + " print(\"Record is already APPROVED — autoApproval may have already processed it.\")\n", + " else:\n", + " print(f\"Record is in {ready_resp['status']} state. Run cleanup first.\")\n", + "\n", + "except TimeoutError as e:\n", + " print(f\"⏰ {e}\")\n", + "except botocore.exceptions.ClientError as e:\n", + " error_code = e.response[\"Error\"][\"Code\"]\n", + " if error_code == \"ConflictException\":\n", + " print(f\"⚠️ Record not in DRAFT state — may already be submitted.\")\n", + " current = cp_client.get_registry_record(registryId=REGISTRY_ID, recordId=A2A_RECORD_ID)\n", + " print(f\" Current status: {current['status']}\")\n", + " else:\n", + " raise" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 7d. Verify Auto-Approval\n", + "\n", + "List registry records to confirm the test record was automatically approved.\n", + "With `autoApproval: True`, the record should transition directly to `APPROVED`\n", + "after submission — no manual intervention needed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Poll for auto-approval to process (up to 60 seconds)\n", + "print(\"Polling for auto-approval (every 5s, up to 60s)...\")\n", + "poll_interval = 5\n", + "poll_timeout = 60\n", + "elapsed = 0\n", + "final_status = None\n", + "\n", + "try:\n", + " while elapsed < poll_timeout:\n", + " auto_record = cp_client.get_registry_record(\n", + " registryId=REGISTRY_ID, recordId=A2A_RECORD_ID\n", + " )\n", + " final_status = auto_record.get(\"status\")\n", + " print(f\" [{elapsed}s] Status: {final_status}\")\n", + "\n", + " if final_status != \"PENDING_APPROVAL\":\n", + " break\n", + "\n", + " time.sleep(poll_interval)\n", + " elapsed += poll_interval\n", + "\n", + " # print(f\"\\nRecord: {AUTO_APPROVAL_RECORD_NAME}\")\n", + " print(f\" Record ID: {A2A_RECORD_ID}\")\n", + " print(f\" Final status: {final_status}\")\n", + " print(f\" Status reason: {auto_record.get('statusReason', 'N/A')}\")\n", + "\n", + " if final_status == \"APPROVED\":\n", + " print(f\"\\n\\u2705 Auto-approval worked! The record was approved automatically.\")\n", + " print(\" No manual intervention was needed.\")\n", + " elif final_status == \"PENDING_APPROVAL\":\n", + " print(f\"\\n\\u23f3 Record is still PENDING_APPROVAL after {poll_timeout}s.\")\n", + " print(\" Try re-running this cell, or check that autoApproval was enabled\")\n", + " print(\" correctly in Section 7b.\")\n", + " else:\n", + " print(f\"\\n\\u26a0\\ufe0f Unexpected status: {final_status}\")\n", + "\n", + "except botocore.exceptions.ClientError as e:\n", + " error_code = e.response[\"Error\"][\"Code\"]\n", + " print(f\"\\u274c Error checking record status: {error_code} \\u2014 {e}\")\n", + " raise\n", + "\n", + "# Also list all records to show the full picture\n", + "print(\"\\n--- All Registry Records ---\")\n", + "all_records_after = utils.list_records_with_ids(cp_client, REGISTRY_ID)\n", + "for rec in all_records_after:\n", + " print(f\" [{rec['status']}] {rec['name']} \\u2014 {rec['recordId']}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 7e. Delete the Auto-Approval Test Record\n", + "\n", + "Clean up the test record created in 7c to keep the registry tidy before restoring the approval configuration." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Delete the auto-approval test record\n", + "try:\n", + " cp_client.delete_registry_record(\n", + " registryId=REGISTRY_ID, recordId=A2A_RECORD_ID\n", + " )\n", + " print(f\"✅ Deleted auto-approval test record: {A2A_RECORD_ID}\")\n", + "except botocore.exceptions.ClientError as e:\n", + " error_code = e.response[\"Error\"][\"Code\"]\n", + " if error_code == \"ResourceNotFoundException\":\n", + " print(f\"Record {A2A_RECORD_ID} already deleted.\")\n", + " else:\n", + " print(f\"❌ Error deleting record: {error_code} — {e}\")\n", + " raise" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 7f. Restore autoApproval to False\n", + "\n", + "Restore the registry to require manual approval so other notebooks (and production\n", + "workflows) continue to work as expected." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " restore_resp = cp_client.update_registry(\n", + " registryId=REGISTRY_ID,\n", + " approvalConfiguration={\"optionalValue\": {\"autoApproval\": False}}\n", + " )\n", + "\n", + " print(f\"✅ Registry restored — autoApproval is now disabled.\")\n", + " print(f\" Updated at: {restore_resp.get('updatedAt', 'N/A')}\")\n", + "\n", + " # Verify the change\n", + " verify_restore = cp_client.get_registry(registryId=REGISTRY_ID)\n", + " verified_auto = verify_restore.get(\"approvalConfiguration\", {}).get(\"autoApproval\", False)\n", + " print(f\" Verified autoApproval: {verified_auto}\")\n", + "\n", + "except botocore.exceptions.ClientError as e:\n", + " error_code = e.response[\"Error\"][\"Code\"]\n", + " print(f\"❌ Error restoring registry: {error_code} — {e}\")\n", + " print(\" ⚠️ IMPORTANT: Manually set autoApproval back to False to avoid\")\n", + " print(\" unintended auto-approvals in other notebooks.\")\n", + " print(\" The cleanup section (Section 8) will also attempt to restore this setting.\")\n", + " raise" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Pre-requisites\n", + "- **Notebook 01** — [Create User Personas](01-create-user-personas-workflow.ipynb): Set up user personas: admin, publisher, consumer\n", + "- **Notebook 02** — [Creating Registry](02-creating-registry-workflow.ipynb): Admin creates a registry\n", + "- **Notebook 03** — [Publishing Records](03-publishing-records-workflow.ipynb): Publish records as a Publisher\n", + "\n", + "## Next Steps\n", + "- **Notebook 05** — [Semantic Search](05-search-registry-workflow.ipynb): Search approved records using NLQ as a Consumer" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "finalchangeenv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/05-search-registry-workflow.ipynb b/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/05-search-registry-workflow.ipynb new file mode 100644 index 000000000..9e1e54764 --- /dev/null +++ b/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/05-search-registry-workflow.ipynb @@ -0,0 +1,851 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 05 - Semantic Search as Consumer\n", + "\n", + "This notebook walks through the **Consumer** persona workflow for discovering agents and tools in AWS Agent Registry using semantic search, then extracting connection metadata for runtime integration.\n", + "\n", + "## What You'll Learn\n", + "\n", + "- Browse the catalog — list registries and records via the control plane (read-only)\n", + "- Verify consumer guardrails — confirm the consumer cannot create, modify, or approve records\n", + "- Semantic search — find agents and tools using natural language queries\n", + "- Filtered search — narrow results by descriptor type, name, and version\n", + "- Extract connection metadata — parse MCP server schemas, A2A agent cards, and custom descriptors\n", + "\n", + "## Prerequisites\n", + "\n", + "- boto3 >= 1.42.87\n", + "- Execute [notebook 01](01-create-user-personas-workflow.ipynb) to create IAM roles for admin, publisher and consumer personas\n", + "- Execute [notebook 02](02-creating-registry-workflow.ipynb) to create registry as Admin\n", + "- Execute [notebook 03](03-publishing-records-workflow.ipynb) to publish records to registry as Publisher\n", + "- Execute [notebook 04](04-admin-approval-workflow.ipynb) for approval workflows\n", + "\n", + "## Semantic Search as Consumer\n", + "![Semantic Search Workflow](images/semantic_search_flow_architecture.png)\n", + "\n", + "## Consumer API References\n", + "\n", + "| # | API | Description |\n", + "|---|-----|-------------|\n", + "| 1 | [ListRegistries](https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-agentcore-control/client/list_registries.html) | Browse available registries (control plane) |\n", + "| 2 | [ListRegistryRecords](https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-agentcore-control/client/list_registry_records.html) | Browse records in a registry (control plane) |\n", + "| 3 | [GetRegistryRecord](https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-agentcore-control/client/get_registry_record.html) | Get full record details (control plane) |\n", + "| 4 | [SearchRegistryRecords](https://docs.aws.amazon.com/boto3/latest/reference/services/bedrock-agentcore/client/search_registry_records.html) | Semantic search over APPROVED records (data plane) |\n", + "\n", + "### Notebook Chain\n", + "\n", + "02 (create registry) → 03 (publish records) → 04 (admin approvals) → **05 (this notebook)**\n", + "\n", + "#### Use Case: Enterprise Payment Processing \n", + "**Consumer Persona:** Once the admin approves the records, AI agents across the AnyCompany enterprise can discover and consume the approved Payment Processing capabilities through flexible search capabilities—including natural language queries (\"find payment processing tools\"), semantic search for conceptually related capabilities, and advanced filtering operators (in, ne, or, and) for precise criteria matching. This enables agents to quickly locate and integrate the payment processing, refund handling, and transaction status tools they need, seamlessly invoking these capabilities without custom integration code to accelerate deployment of payment-enabled customer service experiences across web chat, mobile app, and voice channels." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 1. Install boto3 SDK and dependencies" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Install core dependencies (`boto3` and `python-dotenv`)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install boto3 python-dotenv --force-reinstall" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Initialize boto3 Session as Consumer\n", + "\n", + "Assume the `consumer_persona` IAM role and create a boto3 session with temporary credentials. The consumer has read-only access to registries and records." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os, sys, json, time\n", + "import boto3\n", + "from botocore.exceptions import ClientError\n", + "\n", + "import utils\n", + "\n", + "AWS_REGION = os.environ.get(\"AWS_DEFAULT_REGION\", \"us-west-2\")\n", + "\n", + "# Auto-detect account\n", + "sts = boto3.client(\"sts\", region_name=AWS_REGION)\n", + "ACCOUNT_ID = sts.get_caller_identity()[\"Account\"]\n", + "\n", + "# Assume consumer role\n", + "CONSUMER_ROLE_ARN = f\"arn:aws:iam::{ACCOUNT_ID}:role/consumer_persona\"\n", + "creds = utils.assume_role(role_arn=CONSUMER_ROLE_ARN, session_name=\"consumer-session\")\n", + "\n", + "consumer_session = boto3.Session(\n", + " aws_access_key_id=creds[\"AccessKeyId\"],\n", + " aws_secret_access_key=creds[\"SecretAccessKey\"],\n", + " aws_session_token=creds[\"SessionToken\"],\n", + " region_name=AWS_REGION,\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 3. Initialize the Control Plane Client\n", + "\n", + "The control plane (`bedrock-agentcore-control`) handles CRUD operations for registries and records." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Control plane client (browse, guardrails)\n", + "cp_client = consumer_session.client(\"bedrock-agentcore-control\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Select a Registry\n", + "\n", + "Find an existing READY registry to search. If `REGISTRY_ID` is set, we validate it. Otherwise, we pick the first READY registry." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "REGISTRY_ID = \"\" # Populate this placeholder in case you want to manually pick from above list\n", + "\n", + "# if REGISTRY_ID is left empty, we pick the first READY registry from list_registries\n", + "\n", + "registry_details = utils.get_or_select_registry(cp_client,REGISTRY_ID, AWS_REGION) \n", + "REGISTRY_ID = registry_details[0]\n", + "REGISTRY_ARN = registry_details[1]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 5. Basic Semantic Search\n", + "\n", + "The `SearchRegistryRecords` API uses natural language to find relevant agents and tools.\n", + "It matches against record names, descriptions, and descriptor content.\n", + "\n", + "Key behaviors:\n", + "- Only `APPROVED` records are returned\n", + "- Results are ordered by relevance to the search query\n", + "- The API is on the data plane (`bedrock-agentcore`), not the control plane" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Data plane client (semantic search)\n", + "dp_client = consumer_session.client(\"bedrock-agentcore\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Simple Natural Language Search\n", + "\n", + "Search for records using a natural language query. Results are ranked by relevance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "results = dp_client.search_registry_records(\n", + " registryIds=[REGISTRY_ARN],\n", + " searchQuery=\"refund chargeback analysis\",\n", + " maxResults=5,\n", + ")\n", + "\n", + "print(f\"Found {len(results['registryRecords'])} matching records:\\n\")\n", + "for rec in results[\"registryRecords\"]:\n", + " print(f\" [{rec['status']}] {rec['name']}\")\n", + " print(f\" Type: {rec['descriptorType']}\")\n", + " print(f\" Description: {rec.get('description', 'N/A')}\")\n", + " print(f\" Record ID: {rec['recordId']}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Try Multiple Queries\n", + "\n", + "Run several different natural language queries to see how semantic matching works across different terms." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Try different natural language queries to see how semantic matching works\n", + "queries = [\n", + " \"process a payment\",\n", + " \"refund chargeback analytics\",\n", + " \"credit score risk assessment\",\n", + " \"detect fraudulent transactions\",\n", + " \"billing dispute resolution\",\n", + " \"reconcile payments across systems\",\n", + " \"loan application processing\",\n", + "]\n", + "\n", + "for q in queries:\n", + " results = dp_client.search_registry_records(\n", + " registryIds=[REGISTRY_ARN],\n", + " searchQuery=q,\n", + " maxResults=3,\n", + " )\n", + " count = len(results[\"registryRecords\"])\n", + " print(f\"🔍 '{q}' → {count} result(s)\")\n", + " for rec in results[\"registryRecords\"]:\n", + " print(f\" [{rec['descriptorType']}] {rec['name']}\")\n", + " print()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 6. Filtered Search\n", + "\n", + "The `filters` parameter accepts a JSON document with structured operators to narrow results.\n", + "\n", + "### Supported Filter Fields\n", + "- `descriptorType` — MCP, A2A, CUSTOM, AGENT_SKILLS\n", + "- `name` — exact record name\n", + "- `version` — record version string\n", + "\n", + "### Supported Operators\n", + "\n", + "| Operator | Meaning | Example |\n", + "|----------|---------|---------|\n", + "| `$eq` | Equals | `{\"descriptorType\": {\"$eq\": \"MCP\"}}` |\n", + "| `$ne` | Not equals | `{\"descriptorType\": {\"$ne\": \"CUSTOM\"}}` |\n", + "| `$in` | In list | `{\"descriptorType\": {\"$in\": [\"MCP\", \"A2A\"]}}` |\n", + "| `$and` | Logical AND | `{\"$and\": [filter1, filter2]}` |\n", + "| `$or` | Logical OR | `{\"$or\": [filter1, filter2]}` |" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Filter: only MCP records\n", + "results = dp_client.search_registry_records(\n", + " registryIds=[REGISTRY_ARN],\n", + " searchQuery=\"fraud detection suspicious activity\",\n", + " maxResults=10,\n", + " filters={\"descriptorType\": {\"$eq\": \"MCP\"}},\n", + ")\n", + "\n", + "print(f\"Filter: descriptorType == MCP\")\n", + "print(f\"Results: {len(results['registryRecords'])}\\n\")\n", + "for rec in results[\"registryRecords\"]:\n", + " print(f\" [{rec['descriptorType']}] {rec['name']}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Exclude by Type (`$ne`)\n", + "\n", + "Use the `$ne` (not equals) operator to exclude a specific descriptor type." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Filter: exclude CUSTOM records\n", + "results = dp_client.search_registry_records(\n", + " registryIds=[REGISTRY_ARN],\n", + " searchQuery=\"credit score lending risk\",\n", + " maxResults=10,\n", + " filters={\"descriptorType\": {\"$ne\": \"CUSTOM\"}},\n", + ")\n", + "\n", + "print(f\"Filter: descriptorType != CUSTOM\")\n", + "print(f\"Results: {len(results['registryRecords'])}\\n\")\n", + "for rec in results[\"registryRecords\"]:\n", + " print(f\" [{rec['descriptorType']}] {rec['name']}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Include Multiple Types (`$in`)\n", + "\n", + "Use the `$in` operator to match records of multiple descriptor types." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Filter: MCP or A2A records only\n", + "results = dp_client.search_registry_records(\n", + " registryIds=[REGISTRY_ARN],\n", + " searchQuery=\"billing dispute resolution\",\n", + " maxResults=10,\n", + " filters={\"descriptorType\": {\"$in\": [\"MCP\", \"A2A\"]}},\n", + ")\n", + "\n", + "print(f\"Filter: descriptorType IN [MCP, A2A]\")\n", + "print(f\"Results: {len(results['registryRecords'])}\\n\")\n", + "for rec in results[\"registryRecords\"]:\n", + " print(f\" [{rec['descriptorType']}] {rec['name']}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Compound Filter (`$and`)\n", + "\n", + "Combine multiple conditions with `$and` — here we filter for MCP records with a specific version." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Compound filter: MCP records with a specific version\n", + "results = dp_client.search_registry_records(\n", + " registryIds=[REGISTRY_ARN],\n", + " searchQuery=\"reconcile settlement\",\n", + " maxResults=10,\n", + " filters={\n", + " \"$and\": [\n", + " {\"descriptorType\": {\"$eq\": \"MCP\"}},\n", + " {\"version\": {\"$eq\": \"1.0\"}}\n", + " ]\n", + " },\n", + ")\n", + "\n", + "print(f\"Filter: descriptorType == MCP AND version == 1.0\")\n", + "print(f\"Results: {len(results['registryRecords'])}\\n\")\n", + "for rec in results[\"registryRecords\"]:\n", + " print(f\" [{rec['descriptorType']}] {rec['name']} (v{rec.get('version', '?')})\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### OR Filter (`$or`)\n", + "\n", + "Use `$or` to match records that satisfy any of the given conditions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# OR filter: A2A or CUSTOM records\n", + "results = dp_client.search_registry_records(\n", + " registryIds=[REGISTRY_ARN],\n", + " searchQuery=\"loan application credit check\",\n", + " maxResults=10,\n", + " filters={\n", + " \"$or\": [\n", + " {\"descriptorType\": {\"$eq\": \"A2A\"}},\n", + " {\"descriptorType\": {\"$eq\": \"CUSTOM\"}}\n", + " ]\n", + " },\n", + ")\n", + "\n", + "print(f\"Filter: descriptorType == A2A OR descriptorType == CUSTOM\")\n", + "print(f\"Results: {len(results['registryRecords'])}\\n\")\n", + "for rec in results[\"registryRecords\"]:\n", + " print(f\" [{rec['descriptorType']}] {rec['name']}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 7. Extract Connection Metadata from Search Results\n", + "\n", + "Search results include full `descriptors` — the connection metadata a consumer needs\n", + "to integrate with the discovered agent or tool at runtime.\n", + "\n", + "| Record Type | Descriptor Path | What You Get |\n", + "|-------------|----------------|--------------|\n", + "| MCP | `descriptors.mcp.server.inlineContent` | Server name, packages, transport config |\n", + "| MCP | `descriptors.mcp.tools.inlineContent` | Available tools with input schemas |\n", + "| A2A | `descriptors.a2a.agentCard.inlineContent` | Agent URL, skills, capabilities |\n", + "| CUSTOM | `descriptors.custom.inlineContent` | User-defined endpoint/config |" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Search and extract MCP connection metadata\n", + "results = dp_client.search_registry_records(\n", + " registryIds=[REGISTRY_ARN],\n", + " searchQuery=\"payment\",\n", + " maxResults=5,\n", + " filters={\"descriptorType\": {\"$eq\": \"MCP\"}},\n", + ")\n", + "\n", + "for rec in results[\"registryRecords\"]:\n", + " print(f\"=== MCP Record: {rec['name']} ===\\n\")\n", + "\n", + " mcp = rec.get(\"descriptors\", {}).get(\"mcp\", {})\n", + "\n", + " # Parse server schema\n", + " server_raw = mcp.get(\"server\", {}).get(\"inlineContent\", \"{}\")\n", + " server = json.loads(server_raw) if isinstance(server_raw, str) else server_raw\n", + " print(f\" Server: {server.get('name', 'N/A')}\")\n", + " print(f\" Version: {server.get('version', 'N/A')}\")\n", + " print(f\" Description: {server.get('description', 'N/A')}\")\n", + " for pkg in server.get(\"packages\", []):\n", + " print(f\" Package: {pkg.get('identifier')} ({pkg.get('registryType', 'N/A')})\")\n", + " print(f\" Transport: {pkg.get('transport', {}).get('type', 'N/A')}\")\n", + "\n", + " # Parse tool schema\n", + " tools_raw = mcp.get(\"tools\", {}).get(\"inlineContent\", \"{}\")\n", + " tools = json.loads(tools_raw) if isinstance(tools_raw, str) else tools_raw\n", + " print(f\"\\n Tools ({len(tools.get('tools', []))}):\")\n", + " for tool in tools.get(\"tools\", []):\n", + " params = list(tool.get(\"inputSchema\", {}).get(\"properties\", {}).keys())\n", + " print(f\" • {tool['name']}: {tool.get('description', '')}\")\n", + " print(f\" Parameters: {params}\")\n", + " print()\n", + "\n", + "if not results[\"registryRecords\"]:\n", + " print(\"No MCP records found. Ensure MCP records are APPROVED in the registry.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Extract A2A Agent Card\n", + "\n", + "Parse the `descriptors.a2a.agentCard` to get the agent URL, skills, and capabilities." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Search and extract A2A connection metadata\n", + "results = dp_client.search_registry_records(\n", + " registryIds=[REGISTRY_ARN],\n", + " searchQuery=\"credit score risk assessment agent\",\n", + " maxResults=5,\n", + " filters={\"descriptorType\": {\"$eq\": \"A2A\"}},\n", + ")\n", + "\n", + "for rec in results[\"registryRecords\"]:\n", + " print(f\"=== A2A Record: {rec['name']} ===\\n\")\n", + "\n", + " a2a = rec.get(\"descriptors\", {}).get(\"a2a\", {})\n", + " card_raw = a2a.get(\"agentCard\", {}).get(\"inlineContent\", \"{}\")\n", + " card = json.loads(card_raw) if isinstance(card_raw, str) else card_raw\n", + "\n", + " print(f\" Agent: {card.get('name', 'N/A')}\")\n", + " print(f\" URL: {card.get('url', 'N/A')}\")\n", + " print(f\" Version: {card.get('version', 'N/A')}\")\n", + " print(f\" Description: {card.get('description', 'N/A')}\")\n", + " print(f\"\\n Skills ({len(card.get('skills', []))}):\")\n", + " for skill in card.get(\"skills\", []):\n", + " print(f\" • {skill.get('name', skill.get('id', 'N/A'))}: {skill.get('description', '')}\")\n", + " print()\n", + "\n", + "if not results[\"registryRecords\"]:\n", + " print(\"No A2A records found. This is expected if A2A records haven't been created/approved yet.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Extract CUSTOM Content\n", + "\n", + "Parse the `descriptors.custom.inlineContent` for user-defined endpoint or configuration data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Search and extract CUSTOM connection metadata\n", + "results = dp_client.search_registry_records(\n", + " registryIds=[REGISTRY_ARN],\n", + " searchQuery=\"custom endpoint configuration\",\n", + " maxResults=5,\n", + " filters={\"descriptorType\": {\"$eq\": \"CUSTOM\"}},\n", + ")\n", + "\n", + "for rec in results[\"registryRecords\"]:\n", + " print(f\"=== CUSTOM Record: {rec['name']} ===\\n\")\n", + "\n", + " custom = rec.get(\"descriptors\", {}).get(\"custom\", {})\n", + " content_raw = custom.get(\"inlineContent\", \"{}\")\n", + " content = json.loads(content_raw) if isinstance(content_raw, str) else content_raw\n", + "\n", + " print(f\" Content:\")\n", + " print(json.dumps(content, indent=4))\n", + " print()\n", + "\n", + "if not results[\"registryRecords\"]:\n", + " print(\"No CUSTOM records found. This is expected if CUSTOM records haven't been created/approved yet.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 8. Search Behavior & Edge Cases\n", + "\n", + "Understanding how search behaves in boundary conditions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# maxResults controls how many results are returned (1-20, default 10)\n", + "print(\"=== Pagination: maxResults ===\\n\")\n", + "for n in [1, 3, 10, 20]:\n", + " results = dp_client.search_registry_records(\n", + " registryIds=[REGISTRY_ARN],\n", + " searchQuery=\"transaction processing\",\n", + " maxResults=n,\n", + " )\n", + " print(f\" maxResults={n:<3} → returned {len(results['registryRecords'])} records\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Empty Result Set\n", + "\n", + "A query that matches nothing returns an empty list — no error is raised." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Query that should match nothing\n", + "results = dp_client.search_registry_records(\n", + " registryIds=[REGISTRY_ARN],\n", + " searchQuery=\"quantum teleportation flux capacitor\",\n", + " maxResults=5,\n", + ")\n", + "\n", + "print(f\"Query: 'quantum teleportation flux capacitor'\")\n", + "print(f\"Results: {len(results['registryRecords'])}\\n\")\n", + "if not results[\"registryRecords\"]:\n", + " print(\"✅ Empty result set returned (no error) — this is expected behavior.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## 9. Cleanup (All Notebooks)\n", + "\n", + "This section cleans up all resources created across notebooks 01–05:\n", + "- Delete all registry records (from notebooks 03, 04, 05)\n", + "- Delete the registry (from notebook 02)\n", + "- Delete the three IAM persona roles (from notebook 01)\n", + "\n", + "⚠️ Only run this after you are done with all notebooks. All code is commented out by default." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 1: Assume Admin Role\n", + "\n", + "Admin permissions are required to delete registries, records, and IAM roles." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# # Assume admin role for cleanup\n", + "# admin_creds = utils.assume_role(\n", + "# role_arn=f\"arn:aws:iam::{ACCOUNT_ID}:role/admin_persona\",\n", + "# session_name=\"admin-cleanup-session\",\n", + "# )\n", + "# admin_session = boto3.Session(\n", + "# aws_access_key_id=admin_creds[\"AccessKeyId\"],\n", + "# aws_secret_access_key=admin_creds[\"SecretAccessKey\"],\n", + "# aws_session_token=admin_creds[\"SessionToken\"],\n", + "# region_name=AWS_REGION,\n", + "# )\n", + "# admin_cp = admin_session.client(\"bedrock-agentcore-control\")\n", + "# iam_client = boto3.client(\"iam\", region_name=AWS_REGION)\n", + "# print(\"Admin session ready for cleanup.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 2: Delete All Registry Records\n", + "\n", + "Delete every record in the registry (MCP, A2A, CUSTOM records from notebooks 03 and 04)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# # Delete all records in the registry\n", + "# try:\n", + "# records = admin_cp.list_registry_records(registryId=REGISTRY_ID)\n", + "# all_recs = records.get(\"registryRecords\", [])\n", + "# print(f\"Found {len(all_recs)} record(s) to delete.\\n\")\n", + "# for rec in all_recs:\n", + "# try:\n", + "# admin_cp.delete_registry_record(\n", + "# registryId=REGISTRY_ID, recordId=rec[\"recordId\"]\n", + "# )\n", + "# print(f\" ✅ Deleted: {rec['name']} ({rec['recordId']})\")\n", + "# except Exception as e:\n", + "# print(f\" ⚠️ Failed: {rec['name']} — {e}\")\n", + "# except Exception as e:\n", + "# print(f\"Error listing records: {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 3: Delete the Registry\n", + "\n", + "Delete the registry created in notebook 02. The registry must have no records before deletion." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# # Delete the registry\n", + "# import time\n", + "# try:\n", + "# # Wait for any pending record deletions to complete\n", + "# print(\"Waiting 10s for record deletions to propagate...\")\n", + "# time.sleep(10)\n", + "\n", + "# admin_cp.delete_registry(registryId=REGISTRY_ID)\n", + "# print(f\"✅ Deleted registry: {REGISTRY_ID}\")\n", + "\n", + "# # Wait for deletion to complete\n", + "# print(\"Waiting for registry deletion...\")\n", + "# for _ in range(20):\n", + "# try:\n", + "# r = admin_cp.get_registry(registryId=REGISTRY_ID)\n", + "# print(f\" Status: {r['status']}\")\n", + "# if r[\"status\"] == \"DELETE_FAILED\":\n", + "# print(f\" ❌ Delete failed: {r.get('statusReason', 'unknown')}\")\n", + "# break\n", + "# time.sleep(5)\n", + "# except admin_cp.exceptions.ResourceNotFoundException:\n", + "# print(\" ✅ Registry deleted successfully.\")\n", + "# break\n", + "# except admin_cp.exceptions.ResourceNotFoundException:\n", + "# print(f\"Registry {REGISTRY_ID} not found — already deleted.\")\n", + "# except Exception as e:\n", + "# print(f\"Error deleting registry: {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 4: Delete IAM Persona Roles\n", + "\n", + "Delete the three persona roles (`admin_persona`, `publisher_persona`, `consumer_persona`) created in notebook 01." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# # Delete IAM persona roles\n", + "# PERSONA_ROLES = [\"admin_persona\", \"publisher_persona\", \"consumer_persona\"]\n", + "# POLICY_NAMES = {\"admin_persona\": \"AdminPolicy\", \"publisher_persona\": \"PublisherPolicy\", \"consumer_persona\": \"ConsumerPolicy\"}\n", + "\n", + "# for role_name in PERSONA_ROLES:\n", + "# print(f\"\\nCleaning up: {role_name}\")\n", + "# try:\n", + "# # Delete inline policies\n", + "# try:\n", + "# inline = iam_client.list_role_policies(RoleName=role_name)\n", + "# for policy_name in inline.get(\"PolicyNames\", []):\n", + "# iam_client.delete_role_policy(RoleName=role_name, PolicyName=policy_name)\n", + "# print(f\" Deleted inline policy: {policy_name}\")\n", + "# except iam_client.exceptions.NoSuchEntityException:\n", + "# pass\n", + "\n", + "# # Detach managed policies\n", + "# try:\n", + "# attached = iam_client.list_attached_role_policies(RoleName=role_name)\n", + "# for policy in attached.get(\"AttachedPolicies\", []):\n", + "# iam_client.detach_role_policy(RoleName=role_name, PolicyArn=policy[\"PolicyArn\"])\n", + "# print(f\" Detached: {policy['PolicyArn']}\")\n", + "# except iam_client.exceptions.NoSuchEntityException:\n", + "# pass\n", + "\n", + "# # Delete the role\n", + "# iam_client.delete_role(RoleName=role_name)\n", + "# print(f\" ✅ Deleted role: {role_name}\")\n", + "\n", + "# except iam_client.exceptions.NoSuchEntityException:\n", + "# print(f\" Role {role_name} not found — already deleted.\")\n", + "# except Exception as e:\n", + "# print(f\" ❌ Error: {e}\")\n", + "\n", + "# print(\"\\n🧹 Full cleanup complete.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Pre-requisite Notebooks\n", + "- **Notebook 01** — [Create User Personas](01-create-user-personas-workflow.ipynb): Set up user personas: admin, publisher, consumer\n", + "- **Notebook 02** — [Creating Registry](02-creating-registry-workflow.ipynb): Admin creates a registry\n", + "- **Notebook 03** — [Publishing Records](03-publishing-records-workflow.ipynb): Publish records as a Publisher\n", + "- **Notebook 04** — [Admin Approval](04-admin-approval-workflow.ipynb): Admin Approval workflow " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "| What We Did | API Used | Plane |\n", + "|-------------|----------|-------|\n", + "| Listed registries | `ListRegistries` | Control |\n", + "| Browsed records | `ListRegistryRecords` | Control |\n", + "| Inspected record details | `GetRegistryRecord` | Control |\n", + "| Verified consumer guardrails | Various write APIs | Control |\n", + "| Semantic search (natural language) | `SearchRegistryRecords` | Data |\n", + "| Filtered search (`$eq`, `$ne`, `$in`, `$and`, `$or`) | `SearchRegistryRecords` | Data |\n", + "| Extracted MCP server/tool metadata | Parse `descriptors.mcp` | — |\n", + "| Extracted A2A agent card | Parse `descriptors.a2a` | — |\n", + "| Extracted CUSTOM content | Parse `descriptors.custom` | — |\n", + "\n", + "### Key Takeaways\n", + "\n", + "- **Search only returns APPROVED records** — the approval workflow is the gateway to discoverability\n", + "- **Semantic search matches against names, descriptions, AND descriptor content** — rich descriptors improve findability\n", + "- **Filters use MongoDB-style operators** (`$eq`, `$ne`, `$in`, `$and`, `$or`) on `name`, `descriptorType`, `version`\n", + "- **Consumers are read-only** — they cannot create, modify, approve, or delete anything\n", + "- **Search is eventually consistent** — newly approved records may take 10-30 seconds to appear" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "finalchangeenv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/images/admin_approval_flow_architecture.png b/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/images/admin_approval_flow_architecture.png new file mode 100644 index 000000000..ee80d27c9 Binary files /dev/null and b/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/images/admin_approval_flow_architecture.png differ diff --git a/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/images/publisher_flow_architecture.png b/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/images/publisher_flow_architecture.png new file mode 100644 index 000000000..fd7fd48f5 Binary files /dev/null and b/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/images/publisher_flow_architecture.png differ diff --git a/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/images/registry-architecture.png b/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/images/registry-architecture.png new file mode 100644 index 000000000..27166c1d0 Binary files /dev/null and b/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/images/registry-architecture.png differ diff --git a/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/images/semantic_search_flow_architecture.png b/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/images/semantic_search_flow_architecture.png new file mode 100644 index 000000000..183d1d083 Binary files /dev/null and b/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/images/semantic_search_flow_architecture.png differ diff --git a/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/utils.py b/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/utils.py new file mode 100644 index 000000000..36b1d9f71 --- /dev/null +++ b/01-tutorials/10-Agent-Registry/00-getting-started/step-by-step/utils.py @@ -0,0 +1,209 @@ +import boto3 +import json +import time +import botocore.exceptions + + +def assume_role(role_arn, session_name="my-session"): + """Assume an IAM role and return temporary credentials.""" + sts = boto3.client("sts") + response = sts.assume_role( + RoleArn=role_arn, + RoleSessionName=session_name, + ) + creds = response["Credentials"] + print(f"Assumed role: {response['AssumedRoleUser']['Arn']}") + + return creds + +def assume_role_only(AWS_REGION, role_arn, session_name="test-session"): + """Assume an IAM role""" + sts_client = boto3.client("sts", region_name=AWS_REGION) + response = sts_client.assume_role( + RoleArn=role_arn, + RoleSessionName=session_name, + ) + return response + +def pp(response): + """Pretty-print API response, stripping ResponseMetadata.""" + data = {k: v for k, v in response.items() if k != "ResponseMetadata"} + print(json.dumps(data, indent=2, default=str)) + + +def wait_for_record_ready(publisher_cp_client, registry_id, record_id, interval=5, timeout=120): + """Poll GetRegistryRecord until the record exits CREATING/UPDATING status.""" + deadline = time.time() + timeout + while True: + resp = publisher_cp_client.get_registry_record( + registryId=registry_id, recordId=record_id + ) + status = resp["status"] + print(f" Record {record_id} status: {status}") + if status not in ("CREATING", "UPDATING"): + return resp + if time.time() >= deadline: + raise TimeoutError( + f"Record {record_id} still in {status} after {timeout}s." + ) + time.sleep(interval) + + +print("Helper functions defined: pp, wait_for_record_ready") + +def filter_pending_records(records): + """Return only records with status PENDING_APPROVAL.""" + return [r for r in records if r.get("status") == "PENDING_APPROVAL"] + +def list_records_with_ids(client, registry_id, **kwargs): + """Wrapper around list_registry_records that extracts recordId from raw HTTP response. + + The preview SDK model uses 'registryRecordId' but the service returns 'recordId'. + This function parses the raw JSON to get the actual record IDs. + """ + import json as _json + original_make_request = client._endpoint.make_request + raw_body = {} + + def capture_request(operation_model, request_dict): + result = original_make_request(operation_model, request_dict) + http_response = result[0] + raw_body['data'] = _json.loads(http_response.content.decode('utf-8')) + return result + + client._endpoint.make_request = capture_request + try: + client.list_registry_records(registryId=registry_id, **kwargs) + finally: + client._endpoint.make_request = original_make_request + + return raw_body.get('data', {}).get('registryRecords', []) + + +def get_or_select_registry(cp_client, registry_id=None, AWS_REGION="us-west-2"): + """List registries and return (registry_id, registry_arn) for a READY registry. + + Args: + cp_client: Bedrock AgentCore control plane client. + registry_id: Optional specific registry ID to use. If None, picks the first READY one. + aws_region: AWS region (used in error messages). + + Returns: + Tuple of (registry_id, registry_arn). + + Raises: + ValueError: If specified registry not found or not READY. + RuntimeError: If no READY registry exists. + """ + try: + resp = cp_client.list_registries() + all_registries = resp.get("registries", []) + print(f"Found {len(all_registries)} registries:\n") + for reg in all_registries: + print(f" [{reg['status']}] {reg['name']} ({reg['registryId']})") + + ready = [r for r in all_registries if r["status"] == "READY"] + + if registry_id: + match = [r for r in all_registries if r["registryId"] == registry_id] + if not match: + raise ValueError(f"Registry {registry_id} not found.") + if match[0]["status"] != "READY": + raise ValueError(f"Registry {registry_id} is {match[0]['status']}, not READY.") + rid, rarn = match[0]["registryId"], match[0]["registryArn"] + print(f"\n✅ Using specified registry: {rid}") + elif ready: + rid, rarn = ready[0]["registryId"], ready[0]["registryArn"] + print(f"\n✅ Using registry: {ready[0]['name']} (ID: {rid})") + else: + raise RuntimeError("No READY registry available. Run notebook 02 first.") + + print(f"\nRegistry ID: {rid}") + print(f"Registry ARN: {rarn}") + return rid, rarn + + except botocore.exceptions.EndpointConnectionError as e: + print(f"❌ Cannot reach bedrock-agentcore-control in {AWS_REGION}. Error: {e}") + raise + except botocore.exceptions.ClientError as e: + code = e.response["Error"]["Code"] + print(f"❌ Error listing registries: {code} — {e}") + if code == "AccessDeniedException": + print(" Verify admin_persona has bedrock-agentcore:ListRegistries permission.") + raise + + +def build_trust_policy(sagemaker_role_arn): + """Build a trust policy allowing both the SageMaker role and AgentCore service.""" + return { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "bedrock-agentcore.amazonaws.com"}, + "Action": "sts:AssumeRole", + }, + { + "Effect": "Allow", + "Principal": {"AWS": sagemaker_role_arn}, + "Action": "sts:AssumeRole", + }, + ], + } + + +def build_permissions_policy(actions): + """Build a permissions policy for the given actions.""" + return { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": actions, + "Resource": "*", + } + ], + } + + +def create_or_update_persona_role(iam_client, role_name, policy_name, actions, trust_policy, ACCOUNT_ID): + """Create an IAM role or update it if it already exists.""" + try: + resp = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=json.dumps(trust_policy), + Description=f"AgentCore Registry - {role_name}", + ) + role_arn = resp["Role"]["Arn"] + print(f" Created role: {role_arn}") + except iam_client.exceptions.EntityAlreadyExistsException: + role_arn = f"arn:aws:iam::{ACCOUNT_ID}:role/{role_name}" + print(f" Role already exists: {role_arn} — updating...") + iam_client.update_assume_role_policy( + RoleName=role_name, + PolicyDocument=json.dumps(trust_policy), + ) + + # Attach/update the inline permissions policy + iam_client.put_role_policy( + RoleName=role_name, + PolicyName=policy_name, + PolicyDocument=json.dumps(build_permissions_policy(actions)), + ) + print(f" Attached policy: {policy_name}") + return role_arn + + +def extract_role_arn(caller_arn): + """Get the actual IAM role ARN from the caller identity. + + The assumed-role ARN format loses the role path (e.g., /service-role/). + We extract the role name and look it up via IAM to get the full ARN. + """ + if ":assumed-role/" in caller_arn: + role_name = caller_arn.split(":")[-1].split("/")[1] + # Look up the actual role to get the full ARN with path + iam = boto3.client("iam") + role_info = iam.get_role(RoleName=role_name) + return role_info["Role"]["Arn"] + return caller_arn diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 1d76c6d7a..c3cd74c52 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -108,3 +108,5 @@ - Swara Gandhi - Shubham Gupta (guptashs) - Vibhu Pareek (vibhup) +- Richa Gupta +- Chandra Dhandapani