diff --git a/bun.lock b/bun.lock index a61c220ed..0784241cd 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "name": "agentcore", "dependencies": { "@aws-sdk/client-bedrock-agentcore": "^3.1092.0", - "@aws-sdk/client-bedrock-agentcore-control": "^3.1079.0", + "@aws-sdk/client-bedrock-agentcore-control": "^3.1102.0", "@aws-sdk/client-iam": "^3.1080.0", "@smithy/core": "3.29.3", "@tanstack/react-query": "^5.101.2", @@ -43,7 +43,7 @@ "@aws-sdk/client-bedrock-agentcore": ["@aws-sdk/client-bedrock-agentcore@3.1094.0", "", { "dependencies": { "@aws-sdk/core": "^3.976.0", "@aws-sdk/credential-provider-node": "^3.972.71", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.29.4", "@smithy/fetch-http-handler": "^5.6.6", "@smithy/node-http-handler": "^4.9.6", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-K8+YaYFyaVJvidwVSnmXlHjpOel/qz4ii2TZY2shXbkMoQxftjHuiXlplKeb5pWhzdS092q6YqPR5CZrMH90Ug=="], - "@aws-sdk/client-bedrock-agentcore-control": ["@aws-sdk/client-bedrock-agentcore-control@3.1087.0", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/credential-provider-node": "^3.972.68", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/fetch-http-handler": "^5.6.5", "@smithy/node-http-handler": "^4.9.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-ZCWITtXgDZG1g8NfTZVMTIwH/NR9eifXRW1g5+d4Jb93MmwbJWLWRSd0xqxgywMay0sqhNOKXHE4nQRIf9glag=="], + "@aws-sdk/client-bedrock-agentcore-control": ["@aws-sdk/client-bedrock-agentcore-control@3.1102.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/credential-provider-node": "^3.972.77", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-6HjZkg14iiJr8VinzAKy5XRJe5JLrDg9USBia2UZZAg8leGff2/+cyH0y4ZYQqRe1f1TaMZfecNSQLCy3XInjQ=="], "@aws-sdk/client-iam": ["@aws-sdk/client-iam@3.1087.0", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/credential-provider-node": "^3.972.68", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/fetch-http-handler": "^5.6.5", "@smithy/node-http-handler": "^4.9.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-S2UBBQnPMTREF52WPz7yabvags3rPYLOPwq8LOPNJIekyAIqUwtGN46z2tqGNr+NyGz9cpEI+3brJgkWKQGNIQ=="], @@ -345,17 +345,15 @@ "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/core": ["@aws-sdk/core@3.975.2", "", { "dependencies": { "@aws-sdk/types": "^3.974.1", "@aws-sdk/xml-builder": "^3.972.35", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.29.3", "@smithy/signature-v4": "^5.6.3", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-iyeXwziyjJpixq5OmhsIyrSWx8vwcI7gDo4yRUC3EP7NQtOo9iAJiIEc3G+/HkhtNXqOhofiCK7Lc34Sq+fJWg=="], + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/core": ["@aws-sdk/core@3.977.5", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@aws-sdk/xml-builder": "^3.972.37", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-O5otOc1c6UZh5HsHAaPdYBcUUR9HL6mtnKqvc8nxN/CKDGUBUpsdh0q8K04Uz/dd1i0TaGyIQuQNqoO7+ad2TQ=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.68", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.58", "@aws-sdk/credential-provider-http": "^3.972.60", "@aws-sdk/credential-provider-ini": "^3.973.2", "@aws-sdk/credential-provider-process": "^3.972.58", "@aws-sdk/credential-provider-sso": "^3.973.2", "@aws-sdk/credential-provider-web-identity": "^3.972.64", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/credential-provider-imds": "^4.4.7", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-4akjzW9CjorByYfqXBXmYUh/h7Io3U4DtVgGGh9TQraZ7ZlyJqNyHwDRGiUFnHD+BTOeTbCesCa4sJaK7BGZ7A=="], + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.77", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.66", "@aws-sdk/credential-provider-http": "^3.972.68", "@aws-sdk/credential-provider-ini": "^3.973.11", "@aws-sdk/credential-provider-process": "^3.972.66", "@aws-sdk/credential-provider-sso": "^3.973.10", "@aws-sdk/credential-provider-web-identity": "^3.972.72", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-l4nitYCN/Ls57vtUfdextCjTjW41JD7lQiAnuR0RTbdByFc/6OmEAzwGd+lrp6CUtiXGQL1FCaYiamfHASrwBw=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/types": ["@aws-sdk/types@3.974.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-W0IQZR0eaBqlBFIIofMapaWkw1W0U+Xi4dvW+BqwmCEMd8Ng2U6IhkxuPSjMVnR8klLjfuS9PeZWUl1N6UaZdg=="], + "@aws-sdk/client-bedrock-agentcore-control/@smithy/core": ["@smithy/core@3.31.1", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg=="], - "@aws-sdk/client-bedrock-agentcore-control/@smithy/core": ["@smithy/core@3.29.3", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-L+Ys6ecjk5vwPMAKHBpPKlJ3DkqwNcnfEISXBZIsVvWG/XKXfsAP8mwIYlTeLcd2ElHdesPI8OuOmJSFAPhm6A=="], + "@aws-sdk/client-bedrock-agentcore-control/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A=="], - "@aws-sdk/client-bedrock-agentcore-control/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.5", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-SuqeisTyPoiIPtIYru/sGxGyXzmZ+8nnFOhC+qRPglt06Ebd1yH//CDltZB2J/3WBNVhwfUaZ0EtHB3cm2X32g=="], - - "@aws-sdk/client-bedrock-agentcore-control/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.5", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bNqdxTQTxmLbomSmlkZFz8L6B/feQ2HHzw4L2zY7Ecp2XffYAZq2uzdWDdxJHJFbEvqd+SRuluJso0P8+xPdbw=="], + "@aws-sdk/client-bedrock-agentcore-control/@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.13", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw=="], "@aws-sdk/client-iam/@aws-sdk/core": ["@aws-sdk/core@3.975.2", "", { "dependencies": { "@aws-sdk/types": "^3.974.1", "@aws-sdk/xml-builder": "^3.972.35", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.29.3", "@smithy/signature-v4": "^5.6.3", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-iyeXwziyjJpixq5OmhsIyrSWx8vwcI7gDo4yRUC3EP7NQtOo9iAJiIEc3G+/HkhtNXqOhofiCK7Lc34Sq+fJWg=="], @@ -379,23 +377,23 @@ "react-devtools-core/ws": ["ws@7.5.12", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-1xGnbYN3zbog9CwuNDQULNRrTCLIn46/WmpR1f0w6PsCYQHkylZr5vkd6kfMZYV6pRnQkcPNRyiA8LsrNKyhpg=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.35", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-pXzaWe3evZhjxDXAlMnqISe/XefTCGwBJG4nFTXaWSgAnMkqPEhxEPqJNhhpGesEvKFhvNpnozJJ4GTL11bRYw=="], + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.37", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.4", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-B89bpf2t/y/wia6LZ+4JfHXYQT9PnVftsH05rgJKKIStS7r/4XSs9HOjtPoLtgcA6HCW9jVqX5DBbq7E0PAkiQ=="], + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.58", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-vyGtvK1rY940eq7JT0yIGKuZ+2kpPSJcHibSvGlit5oiMFDamzC7cxBGLl4FLnd6suihMXDI2FSF2dL6TmBqPA=="], + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.66", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bOzP2+zdJ0XrghywB4FaJXtGZCx9yS0AGps+VJ5yEgg30wVyHNmVDBwVDXcRypzQY5iLGCS3NSn0nsuISqjFCQ=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.60", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/fetch-http-handler": "^5.6.5", "@smithy/node-http-handler": "^4.9.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-g9b9YzDrD5pcKiPBJfCSXRfFMrA39eR0guUhZ5SRm+7vMAVc43+effxbcamxBjSd5bUhrdKo5te/yQuWurLXLA=="], + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.68", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-lkunS8X+H6V76WE+t/uGQm/U8v0JXK5mLfNFTUAMlE1kqaCjwlmqKJrgCVtqjK/vqnlrSWsLK4Lr4NANBWlfTQ=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.2", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/credential-provider-env": "^3.972.58", "@aws-sdk/credential-provider-http": "^3.972.60", "@aws-sdk/credential-provider-login": "^3.972.64", "@aws-sdk/credential-provider-process": "^3.972.58", "@aws-sdk/credential-provider-sso": "^3.973.2", "@aws-sdk/credential-provider-web-identity": "^3.972.64", "@aws-sdk/nested-clients": "^3.997.32", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/credential-provider-imds": "^4.4.7", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Yr7yxNyQ8aHt9Ww0RPFUZx+xiem+vl7vuwhP0tniTijoesJNV5jou9HCgVpI0GEPAF+89TkOvilE5uRrZJnjaw=="], + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.11", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/credential-provider-env": "^3.972.66", "@aws-sdk/credential-provider-http": "^3.972.68", "@aws-sdk/credential-provider-login": "^3.972.73", "@aws-sdk/credential-provider-process": "^3.972.66", "@aws-sdk/credential-provider-sso": "^3.973.10", "@aws-sdk/credential-provider-web-identity": "^3.972.72", "@aws-sdk/nested-clients": "^3.997.40", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-KoDEolYtLHG/8C+IiZpXbJWyBOMkrHV+j66Kb9PBXmLv5euGb7aELvuCmLenoGAV6gBW2wM7TsG/1e5iulH4kA=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.58", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-1nYitRCaDmXWUrpBJt6WlcGjLx1JVsMY8rlYuHHsTYTSaYikbixYdQSyINN2VYq1F798uTO9qHAzytL25M8g3A=="], + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.66", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-YOnX6bIhdjx0QfaENu2PB0eFm5MEc9ft8XNGQ+NxMfeLSq9aE+XjWCwDupEnV4UWv5ZFpBLJbTREIx7KNOoqpQ=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.2", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/nested-clients": "^3.997.32", "@aws-sdk/token-providers": "3.1087.0", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-pjMLaLU/JZi5lVfmR14V1OZqRBTuMHf6AwGNZA0K9hK+JKtO3jcLBarfD8iq5oc8cSowvc/9R32sqMVXZPo6xQ=="], + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.10", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/nested-clients": "^3.997.40", "@aws-sdk/token-providers": "3.1102.0", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-IsXnQ35j5VE+3ZK6aIhT5ypB+Jim3zRwVz0nYuVwyBKZyu/SYx+O2/LQpng8c2EiuwyqceabsDlYrICHDlJPsA=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.64", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/nested-clients": "^3.997.32", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-7Buc7p0OvDHW7iBsu4b+YdS0WnaFBDGKDfbVQqaac9dkWiSiUtIoarBDsA1RmOVXZijaZJDoHJFIQiicQvWRlQ=="], + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.72", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/nested-clients": "^3.997.40", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-nj9Zlsy7ya+fy+jhWTJwgfr7YdtDM4xHyZvgKuftuny0UgROVx9lxwvsWJSLvpKk4lig0m0tHng3k1fEnt0LeA=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.4.8", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-q9J7JTiXrAhB8sDp4px97uEPT7CwKH61Co78grdNQvU8QZAdiuaSRhP0tUVf2ogy36RZTrlMU1rBmDEH+cnkiA=="], + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.4.16", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-QfuLWAkLzptffFW980AFeHZFdqds2B64rpEd3uJ6lgs3xVn9QegGMUgUcj+4d7dRrAsya3r58ZKpku97WcFb4w=="], "@aws-sdk/client-iam/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.35", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-pXzaWe3evZhjxDXAlMnqISe/XefTCGwBJG4nFTXaWSgAnMkqPEhxEPqJNhhpGesEvKFhvNpnozJJ4GTL11bRYw=="], @@ -421,15 +419,15 @@ "log-update/wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.64", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/nested-clients": "^3.997.32", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-YQoSI4d6kXvoenoG/0Jv/PqaAuukHzGmGXGyHBQYeEUNsYovlNAn/Sw1wp/WQbhcQ3HsEMGgjEahvD3igz6ecQ=="], + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.73", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/nested-clients": "^3.997.40", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-tjsxMkTAFkmiV9ycmymapb9nLECWVOwFs0bZMQ9gB9bnbY8/HwfukHZlWbXZZp7qkPU6EXAfOcMm3DioFFEywA=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.32", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/signature-v4-multi-region": "^3.996.40", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/fetch-http-handler": "^5.6.5", "@smithy/node-http-handler": "^4.9.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-6Yj2fr9XF67cndITea48rchTdVr3VGx6PN47bIKNinJAjLkmaIlz/4EBPCgJ8UmhVopiXmeAuPLI3+DXDDbMhQ=="], + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.40", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/signature-v4-multi-region": "^3.996.43", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-hEdHT0PBR4fkGxWhwKG5EtEYKnAM7HKkp0vD10ufk4YcXejH4r4q6G/XhPzjUc6Yxo5kBS2vHg7llj4ViR9VTQ=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.32", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/signature-v4-multi-region": "^3.996.40", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/fetch-http-handler": "^5.6.5", "@smithy/node-http-handler": "^4.9.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-6Yj2fr9XF67cndITea48rchTdVr3VGx6PN47bIKNinJAjLkmaIlz/4EBPCgJ8UmhVopiXmeAuPLI3+DXDDbMhQ=="], + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.40", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/signature-v4-multi-region": "^3.996.43", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-hEdHT0PBR4fkGxWhwKG5EtEYKnAM7HKkp0vD10ufk4YcXejH4r4q6G/XhPzjUc6Yxo5kBS2vHg7llj4ViR9VTQ=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1087.0", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/nested-clients": "^3.997.32", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-umM+qNq16f2fH+VLM5MqXW4ORNQAjk+TOSto73xbUHcKaU41L48j786r3UWQYlejeJk37NlvRYgxBT+MBkfaYQ=="], + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1102.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/nested-clients": "^3.997.40", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Ua700vVvM1q105yABSUQWkCK6FeTrNfU6ORGetJe5BzkZWY7QhkF7SVTOlmDGWRDNd6jbyY0Dv5e+E4bMBEmLg=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.32", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/signature-v4-multi-region": "^3.996.40", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/fetch-http-handler": "^5.6.5", "@smithy/node-http-handler": "^4.9.5", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-6Yj2fr9XF67cndITea48rchTdVr3VGx6PN47bIKNinJAjLkmaIlz/4EBPCgJ8UmhVopiXmeAuPLI3+DXDDbMhQ=="], + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.40", "", { "dependencies": { "@aws-sdk/core": "^3.977.5", "@aws-sdk/signature-v4-multi-region": "^3.996.43", "@aws-sdk/types": "^3.974.2", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-hEdHT0PBR4fkGxWhwKG5EtEYKnAM7HKkp0vD10ufk4YcXejH4r4q6G/XhPzjUc6Yxo5kBS2vHg7llj4ViR9VTQ=="], "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.64", "", { "dependencies": { "@aws-sdk/core": "^3.975.2", "@aws-sdk/nested-clients": "^3.997.32", "@aws-sdk/types": "^3.974.1", "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-YQoSI4d6kXvoenoG/0Jv/PqaAuukHzGmGXGyHBQYeEUNsYovlNAn/Sw1wp/WQbhcQ3HsEMGgjEahvD3igz6ecQ=="], @@ -445,11 +443,11 @@ "log-update/cli-cursor/restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.40", "", { "dependencies": { "@aws-sdk/types": "^3.974.1", "@smithy/signature-v4": "^5.6.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-wrGZ/authosokclY1DXsiWT/1WjfCI22FuZGgdcilF+XLTXs5dCjAtiFYSPsEToZkbm3Lj2YP8PoWg0yoMNu0g=="], + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.43", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-lKekx8bLBXSv4O+cslk9Zfnw2XKSkWBs3uWL5QGhH2ZAQfNS7FE0vcSSN2vD/AhxX54ZTywWxR4STThoeOXlBA=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.40", "", { "dependencies": { "@aws-sdk/types": "^3.974.1", "@smithy/signature-v4": "^5.6.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-wrGZ/authosokclY1DXsiWT/1WjfCI22FuZGgdcilF+XLTXs5dCjAtiFYSPsEToZkbm3Lj2YP8PoWg0yoMNu0g=="], + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.43", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-lKekx8bLBXSv4O+cslk9Zfnw2XKSkWBs3uWL5QGhH2ZAQfNS7FE0vcSSN2vD/AhxX54ZTywWxR4STThoeOXlBA=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.40", "", { "dependencies": { "@aws-sdk/types": "^3.974.1", "@smithy/signature-v4": "^5.6.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-wrGZ/authosokclY1DXsiWT/1WjfCI22FuZGgdcilF+XLTXs5dCjAtiFYSPsEToZkbm3Lj2YP8PoWg0yoMNu0g=="], + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.43", "", { "dependencies": { "@aws-sdk/types": "^3.974.2", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-lKekx8bLBXSv4O+cslk9Zfnw2XKSkWBs3uWL5QGhH2ZAQfNS7FE0vcSSN2vD/AhxX54ZTywWxR4STThoeOXlBA=="], "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.40", "", { "dependencies": { "@aws-sdk/types": "^3.974.1", "@smithy/signature-v4": "^5.6.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-wrGZ/authosokclY1DXsiWT/1WjfCI22FuZGgdcilF+XLTXs5dCjAtiFYSPsEToZkbm3Lj2YP8PoWg0yoMNu0g=="], @@ -457,11 +455,11 @@ "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.40", "", { "dependencies": { "@aws-sdk/types": "^3.974.1", "@smithy/signature-v4": "^5.6.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-wrGZ/authosokclY1DXsiWT/1WjfCI22FuZGgdcilF+XLTXs5dCjAtiFYSPsEToZkbm3Lj2YP8PoWg0yoMNu0g=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.6.4", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-B89bpf2t/y/wia6LZ+4JfHXYQT9PnVftsH05rgJKKIStS7r/4XSs9HOjtPoLtgcA6HCW9jVqX5DBbq7E0PAkiQ=="], + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.6.4", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-B89bpf2t/y/wia6LZ+4JfHXYQT9PnVftsH05rgJKKIStS7r/4XSs9HOjtPoLtgcA6HCW9jVqX5DBbq7E0PAkiQ=="], + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], - "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.6.4", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-B89bpf2t/y/wia6LZ+4JfHXYQT9PnVftsH05rgJKKIStS7r/4XSs9HOjtPoLtgcA6HCW9jVqX5DBbq7E0PAkiQ=="], + "@aws-sdk/client-bedrock-agentcore-control/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.6.12", "", { "dependencies": { "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ=="], "@aws-sdk/client-iam/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.6.4", "", { "dependencies": { "@smithy/core": "^3.29.3", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-B89bpf2t/y/wia6LZ+4JfHXYQT9PnVftsH05rgJKKIStS7r/4XSs9HOjtPoLtgcA6HCW9jVqX5DBbq7E0PAkiQ=="], diff --git a/package.json b/package.json index c2fa1da53..19a1e1e5f 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ }, "dependencies": { "@aws-sdk/client-bedrock-agentcore": "^3.1092.0", - "@aws-sdk/client-bedrock-agentcore-control": "^3.1079.0", + "@aws-sdk/client-bedrock-agentcore-control": "^3.1102.0", "@aws-sdk/client-iam": "^3.1080.0", "@smithy/core": "3.29.3", "@tanstack/react-query": "^5.101.2", diff --git a/src/core/gateway.create.test.ts b/src/core/gateway.create.test.ts new file mode 100644 index 000000000..11c2d9e09 --- /dev/null +++ b/src/core/gateway.create.test.ts @@ -0,0 +1,360 @@ +import { describe, expect, test } from "bun:test"; +import { + CreateGatewayCommand, + CreateGatewayRuleCommand, + CreateGatewayTargetCommand, + GetGatewayTargetCommand, + GetGatewayCommand, + GetOauth2CredentialProviderCommand, + ListGatewayTargetsCommand, + type BedrockAgentCoreControlClient, +} from "@aws-sdk/client-bedrock-agentcore-control"; +import type { BedrockAgentCoreClient } from "@aws-sdk/client-bedrock-agentcore"; +import { + CreateRoleCommand, + GetRoleCommand, + PutRolePolicyCommand, + type IAMClient, +} from "@aws-sdk/client-iam"; +import { createSilentLogger } from "../testing"; +import { + gatewayExecutionRoleName, + GATEWAY_EXECUTION_POLICY_NAME, + GATEWAY_ROLE_MANAGED_BY_TAG, + GATEWAY_ROLE_RESOURCE_TYPE_TAG, +} from "./gatewayExecutionRole"; +import { CoreClient } from "./index"; + +type SentCommand = { input: unknown; constructor: { name: string } }; + +function createCore( + send: (command: SentCommand) => Promise, + sendIam: (command: SentCommand) => Promise = async (command) => { + throw new Error(`unexpected ${command.constructor.name}`); + }, +): CoreClient { + return new CoreClient({ + createControlClient: () => ({ send }) as unknown as BedrockAgentCoreControlClient, + createDataClient: () => ({}) as BedrockAgentCoreClient, + createIamClient: () => ({ send: sendIam }) as unknown as IAMClient, + logger: createSilentLogger(), + }); +} + +function noSuchEntity(): Error { + const error = new Error("not found"); + error.name = "NoSuchEntityException"; + return error; +} + +describe("Gateway create Core command mapping", () => { + test("maps MCP and HTTP protocols to their SDK representations", async () => { + const sent: SentCommand[] = []; + const core = createCore(async (command) => { + sent.push(command); + return {}; + }); + + await core.gateway.createGateway( + { + name: "mcp-gateway", + roleArn: "arn:aws:iam::123456789012:role/gateway", + protocol: "mcp", + authorizerType: "AWS_IAM", + }, + { region: "us-west-2" }, + ); + await core.gateway.createGateway( + { + name: "http-gateway", + roleArn: "arn:aws:iam::123456789012:role/gateway", + protocol: "http", + authorizerType: "AWS_IAM", + }, + { region: "us-west-2" }, + ); + + expect(sent[0]).toBeInstanceOf(CreateGatewayCommand); + expect(sent[0]!.input).toEqual({ + name: "mcp-gateway", + roleArn: "arn:aws:iam::123456789012:role/gateway", + protocolType: "MCP", + authorizerType: "AWS_IAM", + }); + expect(sent[1]).toBeInstanceOf(CreateGatewayCommand); + expect(sent[1]!.input).toEqual({ + name: "http-gateway", + roleArn: "arn:aws:iam::123456789012:role/gateway", + authorizerType: "AWS_IAM", + }); + }); + + test("provisions and tightens a default role around Gateway creation", async () => { + const operations: SentCommand[] = []; + const policyNames: string[] = []; + const roleName = gatewayExecutionRoleName("orders", "us-west-2"); + const roleArn = `arn:aws:iam::123456789012:role/${roleName}`; + const gatewayArn = "arn:aws:bedrock-agentcore:us-west-2:123456789012:gateway/orders-abc123"; + const core = createCore( + async (command) => { + operations.push(command); + return { gatewayId: "orders-abc123", gatewayArn }; + }, + async (command) => { + operations.push(command); + if (command instanceof GetRoleCommand) throw noSuchEntity(); + if (command instanceof CreateRoleCommand) { + return { + Role: { + Path: "/", + RoleName: roleName, + RoleId: "AROATEST", + Arn: roleArn, + CreateDate: new Date("2026-08-03T00:00:00Z"), + Tags: [GATEWAY_ROLE_MANAGED_BY_TAG, GATEWAY_ROLE_RESOURCE_TYPE_TAG], + }, + }; + } + if (command instanceof PutRolePolicyCommand) { + policyNames.push((command.input as { PolicyName: string }).PolicyName); + return {}; + } + return {}; + }, + ); + + await core.gateway.createGateway( + { + name: "orders", + protocol: "http", + authorizerType: "AWS_IAM", + policyEngineConfiguration: { + arn: "arn:aws:bedrock-agentcore:us-west-2:123456789012:policy-engine/orders", + mode: "ENFORCE", + }, + }, + { region: "us-west-2" }, + ); + + expect(operations.map((command) => command.constructor.name)).toEqual([ + "GetRoleCommand", + "CreateRoleCommand", + "UpdateAssumeRolePolicyCommand", + "PutRolePolicyCommand", + "CreateGatewayCommand", + "PutRolePolicyCommand", + "UpdateAssumeRolePolicyCommand", + ]); + expect(operations[4]).toBeInstanceOf(CreateGatewayCommand); + expect(operations[4]!.input).toEqual({ + name: "orders", + roleArn, + authorizerType: "AWS_IAM", + policyEngineConfiguration: { + arn: "arn:aws:bedrock-agentcore:us-west-2:123456789012:policy-engine/orders", + mode: "ENFORCE", + }, + }); + expect(policyNames).toEqual([GATEWAY_EXECUTION_POLICY_NAME, GATEWAY_EXECUTION_POLICY_NAME]); + }); + + test("maps Target and Rule create inputs directly", async () => { + const sent: SentCommand[] = []; + const core = createCore(async (command) => { + sent.push(command); + if (command instanceof GetGatewayCommand) { + return { + gatewayId: "gateway-1", + name: "gateway", + roleArn: "arn:aws:iam::123456789012:role/customer-managed", + }; + } + return {}; + }); + const targetConfiguration = { + http: { + agentcoreRuntime: { + arn: "arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/runtime-1", + }, + }, + } as const; + const actions = [{ routeToTarget: { staticRoute: { targetName: "runtime-target" } } }]; + + await core.gateway.createGatewayTarget( + { + gatewayIdentifier: "gateway-1", + targetConfiguration, + }, + { region: "us-west-2" }, + ); + await core.gateway.createGatewayRule( + { + gatewayIdentifier: "gateway-1", + priority: 10, + actions, + }, + { region: "us-west-2" }, + ); + + expect(sent[0]).toBeInstanceOf(GetGatewayCommand); + expect(sent[1]).toBeInstanceOf(CreateGatewayTargetCommand); + expect(sent[1]!.input).toEqual({ + gatewayIdentifier: "gateway-1", + targetConfiguration, + }); + expect(sent[2]).toBeInstanceOf(CreateGatewayRuleCommand); + expect(sent[2]!.input).toEqual({ + gatewayIdentifier: "gateway-1", + priority: 10, + actions, + }); + }); + + test("prepares the union of every paginated Target and deduplicates provider lookups", async () => { + const operations: SentCommand[] = []; + const roleName = gatewayExecutionRoleName("orders", "us-west-2"); + const roleArn = `arn:aws:iam::123456789012:role/${roleName}`; + const oauthProviderArn = + "arn:aws:bedrock-agentcore:us-west-2:123456789012:" + + "token-vault/default/oauth2credentialprovider/shared"; + const workloadIdentityArn = + "arn:aws:bedrock-agentcore:us-west-2:123456789012:" + + "workload-identity-directory/default/workload-identity/orders-identity"; + let storedPolicy: string | undefined; + const core = createCore( + async (command) => { + operations.push(command); + if (command instanceof GetGatewayCommand) { + return { + gatewayId: "gateway-1", + gatewayArn: "arn:aws:bedrock-agentcore:us-west-2:123456789012:gateway/gateway-1", + name: "orders", + roleArn, + workloadIdentityDetails: { workloadIdentityArn }, + }; + } + if (command instanceof ListGatewayTargetsCommand) { + return command.input.nextToken + ? { items: [{ targetId: "oauth-target" }] } + : { items: [{ targetId: "lambda-target" }], nextToken: "page-2" }; + } + if (command instanceof GetGatewayTargetCommand) { + if (command.input.targetId === "lambda-target") { + return { + targetConfiguration: { + mcp: { + lambda: { + lambdaArn: "arn:aws:lambda:us-west-2:123456789012:function:existing", + toolSchema: { inlinePayload: [] }, + }, + }, + }, + }; + } + return { + targetConfiguration: { + mcp: { mcpServer: { endpoint: "https://existing.example.test/mcp" } }, + }, + credentialProviderConfigurations: [ + { + credentialProviderType: "OAUTH", + credentialProvider: { + oauthCredentialProvider: { + providerArn: oauthProviderArn, + scopes: ["read"], + }, + }, + }, + ], + }; + } + if (command instanceof GetOauth2CredentialProviderCommand) { + return { + clientSecretArn: { + secretArn: "arn:aws:secretsmanager:us-west-2:123456789012:secret:shared", + }, + }; + } + if (command instanceof CreateGatewayTargetCommand) { + return { targetId: "new-target", status: "CREATING" }; + } + throw new Error(`unexpected ${command.constructor.name}`); + }, + async (command) => { + operations.push(command); + if (command instanceof GetRoleCommand) { + return { + Role: { + Path: "/", + RoleName: roleName, + RoleId: "AROATEST", + Arn: roleArn, + CreateDate: new Date("2026-08-03T00:00:00Z"), + Tags: [GATEWAY_ROLE_MANAGED_BY_TAG, GATEWAY_ROLE_RESOURCE_TYPE_TAG], + }, + }; + } + if (command instanceof PutRolePolicyCommand) { + storedPolicy = (command.input as { PolicyDocument: string }).PolicyDocument; + expect((command.input as { PolicyName: string }).PolicyName).toBe( + GATEWAY_EXECUTION_POLICY_NAME, + ); + return {}; + } + throw new Error(`unexpected ${command.constructor.name}`); + }, + ); + + await core.gateway.createGatewayTarget( + { + gatewayIdentifier: "gateway-1", + name: "new-target", + targetConfiguration: { + mcp: { mcpServer: { endpoint: "https://new.example.test/mcp" } }, + }, + credentialProviderConfigurations: [ + { + credentialProviderType: "OAUTH", + credentialProvider: { + oauthCredentialProvider: { + providerArn: oauthProviderArn, + scopes: ["write"], + }, + }, + }, + ], + }, + { region: "us-west-2" }, + ); + + const names = operations.map((command) => command.constructor.name); + expect(names).toEqual([ + "GetGatewayCommand", + "GetRoleCommand", + "ListGatewayTargetsCommand", + "GetGatewayTargetCommand", + "ListGatewayTargetsCommand", + "GetGatewayTargetCommand", + "GetOauth2CredentialProviderCommand", + "PutRolePolicyCommand", + "CreateGatewayTargetCommand", + ]); + expect( + operations.filter((command) => command instanceof GetOauth2CredentialProviderCommand), + ).toHaveLength(1); + expect(names.indexOf("PutRolePolicyCommand")).toBeLessThan( + names.indexOf("CreateGatewayTargetCommand"), + ); + const statements = JSON.parse(storedPolicy!).Statement as { + Sid: string; + Resource: string | string[]; + }[]; + expect(statements.find(({ Sid }) => Sid === "AgentCoreGatewayTargetLambda")?.Resource).toEqual([ + "arn:aws:lambda:us-west-2:123456789012:function:existing", + "arn:aws:lambda:us-west-2:123456789012:function:existing:*", + ]); + expect( + statements.find(({ Sid }) => Sid === "AgentCoreGatewayTargetCredentialSecrets")?.Resource, + ).toEqual(["arn:aws:secretsmanager:us-west-2:123456789012:secret:shared"]); + }); +}); diff --git a/src/core/gateway.tsx b/src/core/gateway.tsx index 0e4808176..1582332c3 100644 --- a/src/core/gateway.tsx +++ b/src/core/gateway.tsx @@ -1,10 +1,19 @@ import { + CreateGatewayCommand, + CreateGatewayRuleCommand, + CreateGatewayTargetCommand, GetGatewayCommand, GetGatewayRuleCommand, GetGatewayTargetCommand, + GetApiKeyCredentialProviderCommand, + GetOauth2CredentialProviderCommand, ListGatewayRulesCommand, ListGatewaysCommand, ListGatewayTargetsCommand, + type BedrockAgentCoreControlClient, + type CreateGatewayResponse, + type CreateGatewayRuleResponse, + type CreateGatewayTargetResponse, type GetGatewayResponse, type GetGatewayRuleResponse, type GetGatewayTargetResponse, @@ -12,13 +21,57 @@ import { type ListGatewaysResponse, type ListGatewayTargetsResponse, } from "@aws-sdk/client-bedrock-agentcore-control"; -import type { CoreGatewayClient } from "../handlers/gateway/types"; +import { + buildGatewayCreateRequest, + validateGatewayCreateInput, + validateGatewayTargetCreateInput, +} from "../handlers/gateway/mutations"; +import type { + CoreGatewayClient, + CreateGatewayInput, + CreateGatewayRuleInput, + CreateGatewayTargetInput, +} from "../handlers/gateway/types"; import type { AwsClients, CoreOptions } from "./types"; +import { + ensureGatewayExecutionRole, + getManagedGatewayTargetExecutionRole, + retryWhileGatewayRoleChangesPropagate, + type GatewayTargetRoleConfiguration, +} from "./gatewayExecutionRole"; import { toClientConfig } from "./utils"; export class GatewayClient implements CoreGatewayClient { constructor(private readonly clients: AwsClients) {} + async createGateway( + input: CreateGatewayInput, + options: CoreOptions, + ): Promise { + validateGatewayCreateInput(input); + const control = this.clients.control(toClientConfig(options)); + if (input.roleArn) { + return control.send( + new CreateGatewayCommand(buildGatewayCreateRequest({ ...input, roleArn: input.roleArn })), + ); + } + + const iam = this.clients.iam({ region: options.region }); + const role = await ensureGatewayExecutionRole(iam, input.name!, options.region, input); + await role.updatePolicy(); + const response = await retryWhileGatewayRoleChangesPropagate(() => + control.send( + new CreateGatewayCommand(buildGatewayCreateRequest({ ...input, roleArn: role.roleArn })), + ), + ); + if (!response.gatewayArn) { + throw new Error("CreateGateway response did not include the Gateway ARN"); + } + await role.updatePolicy(response.gatewayArn); + await role.updateTrust(response.gatewayArn); + return response; + } + async getGateway(id: string, options: CoreOptions): Promise { return this.clients .control(toClientConfig(options)) @@ -63,6 +116,33 @@ export class GatewayClient implements CoreGatewayClient { ); } + async createGatewayTarget( + input: CreateGatewayTargetInput, + options: CoreOptions, + ): Promise { + const request = validateGatewayTargetCreateInput(input); + const control = this.clients.control(toClientConfig(options)); + const gateway = await control.send( + new GetGatewayCommand({ gatewayIdentifier: request.gatewayIdentifier }), + ); + const iam = this.clients.iam({ region: options.region }); + const role = await getManagedGatewayTargetExecutionRole(iam, gateway, options.region); + if (!role) { + return control.send(new CreateGatewayTargetCommand(request)); + } + + const configurations = await listTargetRoleConfigurations( + control, + request.gatewayIdentifier!, + undefined, + request, + ); + await role.updatePolicy(configurations); + return retryWhileGatewayRoleChangesPropagate(() => + control.send(new CreateGatewayTargetCommand(request)), + ); + } + async getGatewayRule( gatewayId: string, ruleId: string, @@ -90,4 +170,134 @@ export class GatewayClient implements CoreGatewayClient { }), ); } + + async createGatewayRule( + input: CreateGatewayRuleInput, + options: CoreOptions, + ): Promise { + return this.clients.control(toClientConfig(options)).send(new CreateGatewayRuleCommand(input)); + } +} + +async function listTargetRoleConfigurations( + control: BedrockAgentCoreControlClient, + gatewayId: string, + excludedTargetId?: string, + replacement?: TargetRoleConfigurationSource, +): Promise { + const configurations: GatewayTargetRoleConfiguration[] = []; + const providerSecrets = new Map>(); + let nextToken: string | undefined; + do { + const page = await control.send( + new ListGatewayTargetsCommand({ + gatewayIdentifier: gatewayId, + nextToken, + maxResults: 100, + }), + ); + const targets = await Promise.all( + (page.items ?? []) + .filter(({ targetId }) => targetId && targetId !== excludedTargetId) + .map(({ targetId }) => + control.send( + new GetGatewayTargetCommand({ + gatewayIdentifier: gatewayId, + targetId, + }), + ), + ), + ); + configurations.push( + ...(await Promise.all( + targets.map((target) => targetRoleConfiguration(control, target, providerSecrets)), + )), + ); + nextToken = page.nextToken; + } while (nextToken); + + if (replacement) { + configurations.push(await targetRoleConfiguration(control, replacement, providerSecrets)); + } + return configurations; +} + +type TargetRoleConfigurationSource = Pick< + CreateGatewayTargetInput, + "targetConfiguration" | "credentialProviderConfigurations" +>; + +async function targetRoleConfiguration( + control: BedrockAgentCoreControlClient, + target: TargetRoleConfigurationSource, + providerSecrets = new Map>(), +): Promise { + return { + targetConfiguration: target.targetConfiguration, + credentialProviderConfigurations: target.credentialProviderConfigurations, + credentialProviderSecretArns: await credentialProviderSecretArns( + control, + target.credentialProviderConfigurations, + providerSecrets, + ), + }; +} + +async function credentialProviderSecretArns( + control: BedrockAgentCoreControlClient, + configurations: CreateGatewayTargetInput["credentialProviderConfigurations"], + cache: Map>, +): Promise { + const providers = new Map(); + for (const configuration of configurations ?? []) { + const provider = configuration.credentialProvider; + if ( + configuration.credentialProviderType === "OAUTH" && + provider && + "oauthCredentialProvider" in provider && + provider.oauthCredentialProvider?.providerArn + ) { + providers.set(provider.oauthCredentialProvider.providerArn, "oauth"); + } else if ( + configuration.credentialProviderType === "API_KEY" && + provider && + "apiKeyCredentialProvider" in provider && + provider.apiKeyCredentialProvider?.providerArn + ) { + providers.set(provider.apiKeyCredentialProvider.providerArn, "apiKey"); + } + } + + const secrets = await Promise.all( + [...providers].map(async ([providerArn, type]) => { + const key = `${type}:${providerArn}`; + let lookup = cache.get(key); + if (!lookup) { + lookup = lookupCredentialProviderSecret(control, providerArn, type); + cache.set(key, lookup); + } + return lookup; + }), + ); + return secrets.filter((secretArn): secretArn is string => Boolean(secretArn)); +} + +async function lookupCredentialProviderSecret( + control: BedrockAgentCoreControlClient, + providerArn: string, + type: "oauth" | "apiKey", +): Promise { + const name = credentialProviderName(providerArn); + if (type === "oauth") { + const provider = await control.send(new GetOauth2CredentialProviderCommand({ name })); + return provider.clientSecretArn?.secretArn; + } + const provider = await control.send(new GetApiKeyCredentialProviderCommand({ name })); + return provider.apiKeySecretArn?.secretArn; +} + +function credentialProviderName(providerArn: string): string { + const name = providerArn.split("/").at(-1); + if (!name) throw new Error(`Invalid credential provider ARN "${providerArn}"`); + return name; } diff --git a/src/core/gatewayExecutionRole.test.ts b/src/core/gatewayExecutionRole.test.ts new file mode 100644 index 000000000..bf164743f --- /dev/null +++ b/src/core/gatewayExecutionRole.test.ts @@ -0,0 +1,593 @@ +import { describe, expect, test } from "bun:test"; +import { + CreateRoleCommand, + DeleteRolePolicyCommand, + GetRoleCommand, + PutRolePolicyCommand, + UpdateAssumeRolePolicyCommand, + type IAMClient, +} from "@aws-sdk/client-iam"; +import { + ensureGatewayExecutionRole, + gatewayExecutionRoleName, + GATEWAY_EXECUTION_POLICY_NAME, + GATEWAY_ROLE_MANAGED_BY_TAG, + GATEWAY_ROLE_RESOURCE_TYPE_TAG, + getManagedGatewayTargetExecutionRole, + retryWhileGatewayRoleChangesPropagate, + type GatewayTargetRoleConfiguration, +} from "./gatewayExecutionRole"; + +type SentCommand = { + input: Record; + constructor: { name: string }; +}; + +const ACCOUNT_ID = "123456789012"; +const REGION = "us-west-2"; +const GATEWAY_ARN = "arn:aws:bedrock-agentcore:us-west-2:123456789012:gateway/orders-abc123"; +const POLICY_ENGINE_ARN = + "arn:aws:bedrock-agentcore:us-west-2:123456789012:policy-engine/orders-policy"; +const INTERCEPTOR_ARN = "arn:aws:lambda:us-west-2:123456789012:function:interceptor"; +const TRANSFORM_ARN = "arn:aws:lambda:us-west-2:123456789012:function:transform"; + +function noSuchEntity(): Error { + const error = new Error("not found"); + error.name = "NoSuchEntityException"; + return error; +} + +function alreadyExists(): Error { + const error = new Error("already exists"); + error.name = "EntityAlreadyExistsException"; + return error; +} + +function ownedRole(roleName: string) { + return { + Path: "/", + RoleName: roleName, + RoleId: "AROATEST", + Arn: `arn:aws:iam::${ACCOUNT_ID}:role/${roleName}`, + CreateDate: new Date("2026-08-03T00:00:00Z"), + Tags: [GATEWAY_ROLE_MANAGED_BY_TAG, GATEWAY_ROLE_RESOURCE_TYPE_TAG], + }; +} + +function fakeIam(send: (command: SentCommand) => Promise): IAMClient { + return { send } as unknown as IAMClient; +} + +function requiredString(value: string | undefined): string { + expect(value).toBeString(); + return value!; +} + +function policyStatements(policyDocument: string): Record[] { + return JSON.parse(policyDocument).Statement; +} + +describe("Gateway execution role ownership", () => { + test("derives deterministic collision-resistant names within IAM limits", () => { + const longName = "A".repeat(48); + + expect(gatewayExecutionRoleName(longName, REGION).length).toBeLessThanOrEqual(64); + expect(gatewayExecutionRoleName("orders", REGION)).toBe( + gatewayExecutionRoleName("orders", REGION), + ); + expect(gatewayExecutionRoleName("orders", REGION)).not.toBe( + gatewayExecutionRoleName("Orders", REGION), + ); + expect(gatewayExecutionRoleName("orders", REGION)).not.toBe( + gatewayExecutionRoleName("orders", "us-east-1"), + ); + expect(gatewayExecutionRoleName(`${"a".repeat(32)}-one`, REGION)).not.toBe( + gatewayExecutionRoleName(`${"a".repeat(32)}-two`, REGION), + ); + }); + + test("creates, tags, scopes, and finalizes a least-privilege role", async () => { + const sent: SentCommand[] = []; + const iam = fakeIam(async (command) => { + sent.push(command); + if (command instanceof GetRoleCommand) throw noSuchEntity(); + if (command instanceof CreateRoleCommand) { + return { Role: ownedRole(requiredString(command.input.RoleName)) }; + } + return {}; + }); + + const provisioning = await ensureGatewayExecutionRole(iam, "orders", REGION, { + policyEngineConfiguration: { arn: POLICY_ENGINE_ARN, mode: "ENFORCE" }, + interceptorConfigurations: [ + { + interceptor: { lambda: { arn: INTERCEPTOR_ARN } }, + interceptionPoints: ["REQUEST"], + }, + ], + }); + await provisioning.updatePolicy(); + await provisioning.updateTrust(GATEWAY_ARN); + await provisioning.updatePolicy(GATEWAY_ARN); + + const create = sent.find((command) => command instanceof CreateRoleCommand)!; + const bootstrapTrust = JSON.parse(requiredString(create.input.AssumeRolePolicyDocument)) + .Statement[0]; + expect(bootstrapTrust).toMatchObject({ + Effect: "Deny", + Principal: { Service: "bedrock-agentcore.amazonaws.com" }, + Action: "sts:AssumeRole", + }); + expect(create.input.Tags).toContainEqual(GATEWAY_ROLE_MANAGED_BY_TAG); + expect(create.input.Tags).toContainEqual(GATEWAY_ROLE_RESOURCE_TYPE_TAG); + + const trustUpdates = sent.filter((command) => command instanceof UpdateAssumeRolePolicyCommand); + expect(trustUpdates).toHaveLength(2); + expect( + JSON.parse(requiredString(trustUpdates[0]!.input.PolicyDocument)).Statement[0].Condition, + ).toEqual({ + StringEquals: { "aws:SourceAccount": ACCOUNT_ID }, + ArnLike: { + "aws:SourceArn": `arn:aws:bedrock-agentcore:${REGION}:${ACCOUNT_ID}:gateway/orders-*`, + }, + }); + expect( + JSON.parse(requiredString(trustUpdates[1]!.input.PolicyDocument)).Statement[0].Condition + .ArnLike, + ).toEqual({ "aws:SourceArn": GATEWAY_ARN }); + + const policyUpdates = sent.filter((command) => command instanceof PutRolePolicyCommand); + expect(policyUpdates).toHaveLength(2); + expect(policyUpdates.map(({ input }) => input.PolicyName)).toEqual([ + GATEWAY_EXECUTION_POLICY_NAME, + GATEWAY_EXECUTION_POLICY_NAME, + ]); + const prepared = policyStatements(requiredString(policyUpdates[0]!.input.PolicyDocument)); + const finalized = policyStatements(requiredString(policyUpdates[1]!.input.PolicyDocument)); + expect(prepared.find(({ Sid }) => Sid === "AgentCoreGatewayConfigLambda")).toMatchObject({ + Action: "lambda:InvokeFunction", + Resource: [INTERCEPTOR_ARN], + }); + expect(prepared.find(({ Sid }) => Sid === "AgentCoreGatewayConfigPolicyEngine")).toMatchObject({ + Action: "bedrock-agentcore:GetPolicyEngine", + Resource: POLICY_ENGINE_ARN, + }); + expect( + prepared.find(({ Sid }) => Sid === "AgentCoreGatewayConfigPolicyAuthorization"), + ).toMatchObject({ + Action: ["bedrock-agentcore:AuthorizeAction", "bedrock-agentcore:PartiallyAuthorizeActions"], + Resource: [ + `arn:aws:bedrock-agentcore:${REGION}:${ACCOUNT_ID}:gateway/orders-*`, + POLICY_ENGINE_ARN, + ], + }); + expect(prepared.find(({ Sid }) => Sid === "AgentCoreGatewayConfigGuardrail")).toMatchObject({ + Action: "bedrock:InvokeGuardrailChecks", + Resource: "*", + }); + + expect( + finalized.find(({ Sid }) => Sid === "AgentCoreGatewayConfigPolicyAuthorization")!.Resource, + ).toEqual([GATEWAY_ARN, POLICY_ENGINE_ARN]); + }); + + test("refuses to adopt an untagged role with the generated name", async () => { + const roleName = gatewayExecutionRoleName("orders", REGION); + const iam = fakeIam(async (command) => { + if (command instanceof GetRoleCommand) { + return { Role: { ...ownedRole(roleName), Tags: [] } }; + } + throw new Error(`unexpected ${command.constructor.name}`); + }); + + await expect(ensureGatewayExecutionRole(iam, "orders", REGION, {})).rejects.toThrow( + "not managed by the agentcore CLI", + ); + }); + + test("rechecks ownership after a concurrent creator wins the role-name race", async () => { + const sent: SentCommand[] = []; + const roleName = gatewayExecutionRoleName("orders", REGION); + let getRoleCalls = 0; + const iam = fakeIam(async (command) => { + sent.push(command); + if (command instanceof GetRoleCommand) { + getRoleCalls += 1; + if (getRoleCalls === 1) throw noSuchEntity(); + return { Role: ownedRole(roleName) }; + } + if (command instanceof CreateRoleCommand) throw alreadyExists(); + return {}; + }); + + const provisioning = await ensureGatewayExecutionRole(iam, "orders", REGION, {}); + + expect(provisioning.roleArn).toBe(`arn:aws:iam::${ACCOUNT_ID}:role/${roleName}`); + expect(sent.map((command) => command.constructor.name)).toEqual([ + "GetRoleCommand", + "CreateRoleCommand", + "GetRoleCommand", + "UpdateAssumeRolePolicyCommand", + ]); + }); + + test("omits an inline policy when the Gateway requires no execution permissions", async () => { + const roleName = gatewayExecutionRoleName("orders", REGION); + const sent: SentCommand[] = []; + const iam = fakeIam(async (command) => { + sent.push(command); + if (command instanceof GetRoleCommand) return { Role: ownedRole(roleName) }; + if (command instanceof DeleteRolePolicyCommand) throw noSuchEntity(); + return {}; + }); + + const provisioning = await ensureGatewayExecutionRole(iam, "orders", REGION, {}); + await provisioning.updatePolicy(); + await provisioning.updatePolicy(GATEWAY_ARN); + + expect(sent.filter((command) => command instanceof PutRolePolicyCommand)).toHaveLength(0); + expect(sent.filter((command) => command instanceof DeleteRolePolicyCommand)).toHaveLength(2); + }); + + test("derives least-privilege permissions for modeled Target resources", async () => { + const roleName = gatewayExecutionRoleName("orders", REGION); + const sent: SentCommand[] = []; + const iam = fakeIam(async (command) => { + sent.push(command); + if (command instanceof GetRoleCommand) { + return { Role: ownedRole(roleName) }; + } + if (command instanceof PutRolePolicyCommand) return {}; + throw new Error(`unexpected ${command.constructor.name}`); + }); + const lambdaArn = "arn:aws:lambda:us-west-2:123456789012:function:target"; + const runtimeArn = "arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/runtime-1"; + const oauthProviderArn = + "arn:aws:bedrock-agentcore:us-west-2:123456789012:token-vault/default/oauth2credentialprovider/oauth"; + const apiKeyProviderArn = + "arn:aws:bedrock-agentcore:us-west-2:123456789012:token-vault/default/apikeycredentialprovider/key"; + const workloadIdentityArn = + "arn:aws:bedrock-agentcore:us-west-2:123456789012:" + + "workload-identity-directory/default/workload-identity/orders-workload"; + const workloadIdentityDirectoryArn = + "arn:aws:bedrock-agentcore:us-west-2:123456789012:workload-identity-directory/default"; + const tokenVaultArn = "arn:aws:bedrock-agentcore:us-west-2:123456789012:token-vault/default"; + const configurations: GatewayTargetRoleConfiguration[] = [ + { + targetConfiguration: { + mcp: { + lambda: { + lambdaArn, + toolSchema: { s3: { uri: "s3://schemas/lambda.json" } }, + }, + }, + }, + }, + { + targetConfiguration: { + mcp: { + connector: { + source: { connectorId: "bedrock-knowledge-bases" }, + configurations: [ + { + name: "Retrieve", + parameterValues: { knowledgeBaseId: "KB12345678" }, + }, + { + name: "AgenticRetrieveStream", + parameterValues: { + retrievers: [ + { + configuration: { + knowledgeBase: { knowledgeBaseId: "KB87654321" }, + }, + }, + ], + }, + }, + ], + }, + }, + }, + credentialProviderConfigurations: [{ credentialProviderType: "GATEWAY_IAM_ROLE" }], + }, + { + targetConfiguration: { + inference: { + connector: { + source: { connectorId: "bedrock-mantle" }, + }, + }, + }, + credentialProviderConfigurations: [{ credentialProviderType: "GATEWAY_IAM_ROLE" }], + }, + { + targetConfiguration: { + mcp: { + openApiSchema: { s3: { uri: "s3://schemas/openapi.json" } }, + }, + }, + }, + { + targetConfiguration: { + mcp: { + apiGateway: { + restApiId: "api123", + stage: "prod", + apiGatewayToolConfiguration: { toolFilters: [] }, + }, + }, + }, + credentialProviderConfigurations: [{ credentialProviderType: "GATEWAY_IAM_ROLE" }], + }, + { + targetConfiguration: { + http: { + agentcoreRuntime: { + arn: runtimeArn, + qualifier: "LIVE", + schema: { source: { s3: { uri: "s3://schemas/runtime.json" } } }, + }, + }, + }, + }, + { + targetConfiguration: { + mcp: { + connector: { + source: { connectorId: "web-search" }, + }, + }, + }, + }, + { + targetConfiguration: { + mcp: { mcpServer: { endpoint: "https://example.com/mcp" } }, + }, + credentialProviderConfigurations: [ + { + credentialProviderType: "OAUTH", + credentialProvider: { + oauthCredentialProvider: { + providerArn: oauthProviderArn, + scopes: ["read"], + grantType: "TOKEN_EXCHANGE", + }, + }, + }, + { + credentialProviderType: "API_KEY", + credentialProvider: { + apiKeyCredentialProvider: { + providerArn: apiKeyProviderArn, + }, + }, + }, + ], + credentialProviderSecretArns: [ + "arn:aws:secretsmanager:us-west-2:123456789012:secret:oauth", + "arn:aws:secretsmanager:us-west-2:123456789012:secret:api-key", + ], + }, + ]; + + const role = await getManagedGatewayTargetExecutionRole( + iam, + { + gatewayId: "orders-abc123", + gatewayArn: GATEWAY_ARN, + name: "orders", + roleArn: `arn:aws:iam::${ACCOUNT_ID}:role/${roleName}`, + workloadIdentityDetails: { workloadIdentityArn }, + interceptorConfigurations: [ + { + interceptor: { lambda: { arn: INTERCEPTOR_ARN } }, + interceptionPoints: ["REQUEST"], + }, + ], + customTransformConfiguration: { lambda: { arn: TRANSFORM_ARN } }, + } as Parameters[1], + REGION, + ); + await role!.updatePolicy(configurations); + + const policyUpdate = sent.find((command) => command instanceof PutRolePolicyCommand); + const statements = policyStatements(requiredString(policyUpdate?.input.PolicyDocument)); + expect(statements.find(({ Sid }) => Sid === "AgentCoreGatewayConfigLambda")).toMatchObject({ + Action: "lambda:InvokeFunction", + Resource: [INTERCEPTOR_ARN, TRANSFORM_ARN].sort(), + }); + expect(statements.find(({ Sid }) => Sid === "AgentCoreGatewayTargetLambda")).toMatchObject({ + Action: "lambda:InvokeFunction", + Resource: [lambdaArn, `${lambdaArn}:*`], + }); + expect(statements.find(({ Sid }) => Sid === "AgentCoreGatewayTargetS3Schema")).toMatchObject({ + Action: "s3:GetObject", + Resource: [ + "arn:aws:s3:::schemas/lambda.json", + "arn:aws:s3:::schemas/openapi.json", + "arn:aws:s3:::schemas/runtime.json", + ], + }); + expect(statements.find(({ Sid }) => Sid === "AgentCoreGatewayTargetApiGateway")).toMatchObject({ + Action: "execute-api:Invoke", + Resource: ["arn:aws:execute-api:us-west-2:123456789012:api123/prod/*/*"], + }); + expect(statements.find(({ Sid }) => Sid === "AgentCoreGatewayTargetRuntime")).toMatchObject({ + Action: "bedrock-agentcore:InvokeAgentRuntime", + Resource: [runtimeArn, `${runtimeArn}/runtime-endpoint/LIVE`], + }); + expect(statements.find(({ Sid }) => Sid === "AgentCoreGatewayTargetWebSearch")).toMatchObject({ + Action: "bedrock-agentcore:InvokeWebSearch", + Resource: "arn:aws:bedrock-agentcore:us-west-2:aws:tool/web-search.v1", + }); + expect( + statements.find(({ Sid }) => Sid === "AgentCoreGatewayTargetKnowledgeBase"), + ).toMatchObject({ + Action: ["bedrock:GetKnowledgeBase", "bedrock:Retrieve"], + Resource: [ + "arn:aws:bedrock:us-west-2:123456789012:knowledge-base/KB12345678", + "arn:aws:bedrock:us-west-2:123456789012:knowledge-base/KB87654321", + ], + }); + expect( + statements.find(({ Sid }) => Sid === "AgentCoreGatewayTargetAgenticRetrieve"), + ).toMatchObject({ + Action: "bedrock:AgenticRetrieveStream", + Resource: "*", + }); + expect( + statements.find(({ Sid }) => Sid === "AgentCoreGatewayTargetBedrockMantle"), + ).toMatchObject({ + Action: "bedrock-mantle:CreateInference", + Resource: "*", + }); + expect( + statements.find(({ Sid }) => Sid === "AgentCoreGatewayTargetWorkloadIdentity"), + ).toMatchObject({ + Action: [ + "bedrock-agentcore:GetWorkloadAccessToken", + "bedrock-agentcore:GetWorkloadAccessTokenForJWT", + "bedrock-agentcore:GetWorkloadAccessTokenForUserId", + ], + Resource: [workloadIdentityDirectoryArn, workloadIdentityArn], + }); + const oauthResources = [ + tokenVaultArn, + oauthProviderArn, + workloadIdentityDirectoryArn, + workloadIdentityArn, + ].sort(); + expect( + statements.find(({ Sid }) => Sid === "AgentCoreGatewayTargetOAuthCompleteAuth"), + ).toMatchObject({ + Action: "bedrock-agentcore:CompleteResourceTokenAuth", + Resource: oauthResources, + }); + expect(statements.find(({ Sid }) => Sid === "AgentCoreGatewayTargetOAuth")).toMatchObject({ + Action: "bedrock-agentcore:GetResourceOauth2Token", + Resource: oauthResources, + }); + expect(statements.find(({ Sid }) => Sid === "AgentCoreGatewayTargetApiKey")).toMatchObject({ + Action: "bedrock-agentcore:GetResourceApiKey", + Resource: [ + tokenVaultArn, + apiKeyProviderArn, + workloadIdentityDirectoryArn, + workloadIdentityArn, + ].sort(), + }); + expect( + statements.find(({ Sid }) => Sid === "AgentCoreGatewayTargetCredentialSecrets"), + ).toMatchObject({ + Action: "secretsmanager:GetSecretValue", + Resource: [ + "arn:aws:secretsmanager:us-west-2:123456789012:secret:api-key", + "arn:aws:secretsmanager:us-west-2:123456789012:secret:oauth", + ], + }); + }); + + test("requires a customer-managed role when SigV4 permissions cannot be inferred", async () => { + const roleName = gatewayExecutionRoleName("orders", REGION); + const iam = fakeIam(async (command) => { + if (command instanceof GetRoleCommand) return { Role: ownedRole(roleName) }; + throw new Error(`unexpected ${command.constructor.name}`); + }); + const role = await getManagedGatewayTargetExecutionRole( + iam, + { + gatewayId: "orders-abc123", + gatewayArn: GATEWAY_ARN, + name: "orders", + roleArn: `arn:aws:iam::${ACCOUNT_ID}:role/${roleName}`, + } as Parameters[1], + REGION, + ); + + for (const configuration of [ + { + targetConfiguration: { + http: { + passthrough: { + endpoint: "https://service.us-west-2.amazonaws.com", + protocolType: "CUSTOM", + }, + }, + }, + credentialProviderConfigurations: [ + { + credentialProviderType: "GATEWAY_IAM_ROLE", + credentialProvider: { + iamCredentialProvider: { + service: "service", + region: REGION, + }, + }, + }, + ], + }, + { + targetConfiguration: { + mcp: { + smithyModel: { + inlinePayload: JSON.stringify({ + smithy: "2.0", + shapes: {}, + }), + }, + }, + }, + }, + ] satisfies GatewayTargetRoleConfiguration[]) { + await expect(role!.updatePolicy([configuration])).rejects.toThrow( + "customer-managed --role-arn", + ); + } + }); +}); + +describe("Gateway role propagation retry", () => { + test("retries only role-assumption validation failures up to the configured limit", async () => { + let attempts = 0; + const delays: number[] = []; + + const result = await retryWhileGatewayRoleChangesPropagate( + async () => { + attempts += 1; + if (attempts < 3) { + const error = new Error("The execution role cannot be assumed yet"); + error.name = "ValidationException"; + throw error; + } + return "created"; + }, + 3, + 25, + async (delay) => { + delays.push(delay); + }, + ); + + expect(result).toBe("created"); + expect(attempts).toBe(3); + expect(delays).toEqual([25, 25]); + }); + + test("does not retry unrelated failures", async () => { + let attempts = 0; + + await expect( + retryWhileGatewayRoleChangesPropagate( + async () => { + attempts += 1; + throw new Error("access denied"); + }, + 3, + 0, + async () => {}, + ), + ).rejects.toThrow("access denied"); + expect(attempts).toBe(1); + }); +}); diff --git a/src/core/gatewayExecutionRole.ts b/src/core/gatewayExecutionRole.ts new file mode 100644 index 000000000..0315ffff6 --- /dev/null +++ b/src/core/gatewayExecutionRole.ts @@ -0,0 +1,296 @@ +import { createHash } from "node:crypto"; +import type { GetGatewayResponse } from "@aws-sdk/client-bedrock-agentcore-control"; +import { + CreateRoleCommand, + DeleteRolePolicyCommand, + GetRoleCommand, + PutRolePolicyCommand, + UpdateAssumeRolePolicyCommand, + type IAMClient, + type Role, +} from "@aws-sdk/client-iam"; +import { InputValidationError } from "../errors"; +import { + gatewayPolicyDocument, + gatewayTargetPolicyDocument, + type GatewayRoleConfiguration, + type GatewayTargetPolicyContext, + type GatewayTargetRoleConfiguration, +} from "./gatewayExecutionRolePolicy"; + +export type { GatewayTargetRoleConfiguration } from "./gatewayExecutionRolePolicy"; + +export const GATEWAY_EXECUTION_POLICY_NAME = "AgentCoreGatewayExecutionPolicy"; +export const GATEWAY_ROLE_MANAGED_BY_TAG = { + Key: "bedrock-agentcore:managed-by", + Value: "agentcore-cli", +} as const; +export const GATEWAY_ROLE_RESOURCE_TYPE_TAG = { + Key: "bedrock-agentcore:resource-type", + Value: "gateway", +} as const; + +export type GatewayExecutionRoleProvisioning = { + roleArn: string; + roleName: string; + updatePolicy(gatewayArn?: string): Promise; + updateTrust(gatewayArn: string): Promise; +}; + +export interface GatewayTargetExecutionRole { + roleName: string; + updatePolicy(configurations: GatewayTargetRoleConfiguration[]): Promise; +} + +export function gatewayExecutionRoleName(gatewayName: string, region: string): string { + const suffix = createHash("sha256").update(`${region}:${gatewayName}`).digest("hex").slice(0, 12); + return `AgentCoreGateway-${gatewayName.slice(0, 32)}-${suffix}`; +} + +export async function ensureGatewayExecutionRole( + iam: IAMClient, + gatewayName: string, + region: string, + configuration: GatewayRoleConfiguration, +): Promise { + const roleName = gatewayExecutionRoleName(gatewayName, region); + const role = await getOrCreateOwnedRole(iam, roleName, gatewayName); + const roleArn = requiredRoleArn(role, roleName); + const arn = parseRoleArn(roleArn); + const gatewayPattern = gatewayArnPattern(arn.partition, region, arn.accountId, gatewayName); + + await iam.send( + new UpdateAssumeRolePolicyCommand({ + RoleName: roleName, + PolicyDocument: trustPolicy(arn.accountId, gatewayPattern), + }), + ); + + return { + roleArn, + roleName, + updatePolicy: async (gatewayArn = gatewayPattern) => + await replaceGatewayExecutionPolicy( + iam, + roleName, + gatewayPolicyDocument(configuration, gatewayArn), + ), + updateTrust: async (gatewayArn) => { + await iam.send( + new UpdateAssumeRolePolicyCommand({ + RoleName: roleName, + PolicyDocument: trustPolicy(arn.accountId, gatewayArn), + }), + ); + }, + }; +} + +export async function retryWhileGatewayRoleChangesPropagate( + operation: () => Promise, + attempts = 8, + delayMs = 2000, + sleep: (delayMs: number) => Promise = (delay) => + new Promise((resolve) => setTimeout(resolve, delay)), +): Promise { + for (let attempt = 1; ; attempt += 1) { + try { + return await operation(); + } catch (error) { + const retryable = isGatewayRolePropagationError(error); + if (!retryable || attempt >= attempts) throw error; + await sleep(delayMs); + } + } +} + +export async function getManagedGatewayTargetExecutionRole( + iam: IAMClient, + gateway: GetGatewayResponse, + region: string, +): Promise { + if (!gateway.roleArn || !gateway.name) return undefined; + + const parsedArn = parseRoleArn(gateway.roleArn); + const expectedRoleName = gatewayExecutionRoleName(gateway.name, region); + if (parsedArn.roleName !== expectedRoleName) return undefined; + + const response = await iam.send(new GetRoleCommand({ RoleName: parsedArn.roleName })); + if (!isOwnedGatewayRole(response.Role)) return undefined; + + const gatewayId = gateway.gatewayId ?? gateway.gatewayArn?.split("/").at(-1); + if (!gatewayId) { + throw new Error("GetGateway response did not include the Gateway ID"); + } + const context: GatewayTargetPolicyContext = { + partition: parsedArn.partition, + region, + accountId: parsedArn.accountId, + gatewayId, + workloadIdentityArn: gateway.workloadIdentityDetails?.workloadIdentityArn, + }; + const gatewayArn = + gateway.gatewayArn ?? + `arn:${parsedArn.partition}:bedrock-agentcore:${region}:${parsedArn.accountId}:gateway/${gatewayId}`; + + return { + roleName: parsedArn.roleName, + updatePolicy: async (configurations) => + await replaceGatewayExecutionPolicy( + iam, + parsedArn.roleName, + gatewayTargetPolicyDocument(gateway, gatewayArn, configurations, context), + ), + }; +} + +async function replaceGatewayExecutionPolicy( + iam: IAMClient, + roleName: string, + policyDocument: string | undefined, +): Promise { + if (policyDocument) { + await iam.send( + new PutRolePolicyCommand({ + RoleName: roleName, + PolicyName: GATEWAY_EXECUTION_POLICY_NAME, + PolicyDocument: policyDocument, + }), + ); + return; + } + + try { + await iam.send( + new DeleteRolePolicyCommand({ + RoleName: roleName, + PolicyName: GATEWAY_EXECUTION_POLICY_NAME, + }), + ); + } catch (error) { + if ((error as Error).name !== "NoSuchEntityException") throw error; + } +} + +async function getOrCreateOwnedRole( + iam: IAMClient, + roleName: string, + gatewayName: string, +): Promise { + try { + const existing = await iam.send(new GetRoleCommand({ RoleName: roleName })); + assertOwnedGatewayRole(existing.Role, roleName); + return existing.Role!; + } catch (error) { + if ((error as Error).name !== "NoSuchEntityException") throw error; + } + + try { + const created = await iam.send( + new CreateRoleCommand({ + RoleName: roleName, + AssumeRolePolicyDocument: bootstrapTrustPolicy(), + Description: `Execution role for the AgentCore Gateway "${gatewayName}" created by the agentcore CLI`, + Tags: [ + GATEWAY_ROLE_MANAGED_BY_TAG, + GATEWAY_ROLE_RESOURCE_TYPE_TAG, + { Key: "bedrock-agentcore:resource-name", Value: gatewayName }, + ], + }), + ); + return created.Role!; + } catch (error) { + if ((error as Error).name !== "EntityAlreadyExistsException") throw error; + const existing = await iam.send(new GetRoleCommand({ RoleName: roleName })); + assertOwnedGatewayRole(existing.Role, roleName); + return existing.Role!; + } +} + +function assertOwnedGatewayRole(role: Role | undefined, roleName: string): void { + if (isOwnedGatewayRole(role)) return; + throw new InputValidationError( + `IAM role "${roleName}" already exists but is not managed by the agentcore CLI; ` + + "pass --role-arn with a customer-managed Gateway role", + ); +} + +function isOwnedGatewayRole(role: Role | undefined): boolean { + const tags = new Map(role?.Tags?.map(({ Key, Value }) => [Key, Value])); + return ( + tags.get(GATEWAY_ROLE_MANAGED_BY_TAG.Key) === GATEWAY_ROLE_MANAGED_BY_TAG.Value && + tags.get(GATEWAY_ROLE_RESOURCE_TYPE_TAG.Key) === GATEWAY_ROLE_RESOURCE_TYPE_TAG.Value + ); +} + +function requiredRoleArn(role: Role, roleName: string): string { + if (!role.Arn) { + throw new Error(`IAM did not return an ARN for role "${roleName}"`); + } + return role.Arn; +} + +function bootstrapTrustPolicy(): string { + return JSON.stringify({ + Version: "2012-10-17", + Statement: [ + { + Sid: "GatewayAssumeRoleBootstrap", + Effect: "Deny", + Principal: { Service: "bedrock-agentcore.amazonaws.com" }, + Action: "sts:AssumeRole", + }, + ], + }); +} + +function trustPolicy(accountId: string, sourceArn: string): string { + return JSON.stringify({ + Version: "2012-10-17", + Statement: [ + { + Sid: "GatewayAssumeRolePolicy", + Effect: "Allow", + Principal: { Service: "bedrock-agentcore.amazonaws.com" }, + Action: "sts:AssumeRole", + Condition: { + StringEquals: { "aws:SourceAccount": accountId }, + ArnLike: { "aws:SourceArn": sourceArn }, + }, + }, + ], + }); +} + +function isGatewayRolePropagationError(error: unknown): boolean { + if ((error as Error).name !== "ValidationException") return false; + const message = (error as Error).message ?? ""; + return [ + /\b(?:execution|gateway|iam)\s+role\b.*\b(?:assum|trust)\w*/i, + /\b(?:assum|trust)\w*\b.*\brole\b/i, + /\brole\b.*\b(?:lack|missing|permission|authoriz)\w*/i, + /\b(?:permission|authoriz)\w*\b.*\brole\b/i, + ].some((pattern) => pattern.test(message)); +} + +function parseRoleArn(arn: string): { + partition: string; + accountId: string; + roleName: string; +} { + const match = /^arn:([^:]+):iam::(\d{12}):role\/(.+)$/.exec(arn); + const roleName = match?.[3]?.split("/").at(-1); + if (!match || !roleName) { + throw new InputValidationError(`Invalid IAM role ARN "${arn}"`); + } + return { partition: match[1]!, accountId: match[2]!, roleName }; +} + +function gatewayArnPattern( + partition: string, + region: string, + accountId: string, + gatewayName: string, +): string { + return `arn:${partition}:bedrock-agentcore:${region}:${accountId}:gateway/${gatewayName}-*`; +} diff --git a/src/core/gatewayExecutionRolePolicy.ts b/src/core/gatewayExecutionRolePolicy.ts new file mode 100644 index 000000000..8787ad6ac --- /dev/null +++ b/src/core/gatewayExecutionRolePolicy.ts @@ -0,0 +1,505 @@ +import type { + CreateGatewayTargetRequest, + GetGatewayResponse, +} from "@aws-sdk/client-bedrock-agentcore-control"; +import { InputValidationError } from "../errors"; + +const GATEWAY_POLICY_SCOPE = "AgentCoreGatewayConfig"; +const GATEWAY_TARGET_POLICY_SCOPE = "AgentCoreGatewayTarget"; + +export type GatewayRoleConfiguration = { + customTransformConfiguration?: GetGatewayResponse["customTransformConfiguration"]; + interceptorConfigurations?: GetGatewayResponse["interceptorConfigurations"]; + policyEngineConfiguration?: GetGatewayResponse["policyEngineConfiguration"]; +}; + +export type GatewayTargetRoleConfiguration = Pick< + CreateGatewayTargetRequest, + "targetConfiguration" | "credentialProviderConfigurations" +> & { + credentialProviderSecretArns?: string[]; +}; + +export type GatewayTargetPolicyContext = { + partition: string; + region: string; + accountId: string; + gatewayId: string; + workloadIdentityArn?: string; +}; + +type PolicyStatement = { + Sid?: string; + Effect?: string; + Action?: string | string[]; + Resource?: string | string[]; + [key: string]: unknown; +}; + +type PolicyDocument = { + Version: "2012-10-17"; + Statement: PolicyStatement[]; +}; + +export function gatewayPolicyDocument( + configuration: GatewayRoleConfiguration, + gatewayArn: string, +): string | undefined { + return policyDocumentForStatements(configurationStatements(configuration, gatewayArn)); +} + +export function gatewayTargetPolicyDocument( + gateway: GatewayRoleConfiguration, + gatewayArn: string, + configurations: GatewayTargetRoleConfiguration[], + context: GatewayTargetPolicyContext, +): string | undefined { + return policyDocumentForStatements([ + ...configurationStatements(gateway, gatewayArn), + ...targetStatements(configurations, context), + ]); +} + +function configurationStatements( + configuration: GatewayRoleConfiguration, + gatewayArn: string, +): PolicyStatement[] { + const statements: PolicyStatement[] = []; + const policyEngineArn = configuration.policyEngineConfiguration?.arn; + if (policyEngineArn) { + statements.push( + { + Sid: `${GATEWAY_POLICY_SCOPE}PolicyEngine`, + Effect: "Allow", + Action: "bedrock-agentcore:GetPolicyEngine", + Resource: policyEngineArn, + }, + { + Sid: `${GATEWAY_POLICY_SCOPE}PolicyAuthorization`, + Effect: "Allow", + Action: [ + "bedrock-agentcore:AuthorizeAction", + "bedrock-agentcore:PartiallyAuthorizeActions", + ], + Resource: [policyEngineArn, gatewayArn].sort(), + }, + { + Sid: `${GATEWAY_POLICY_SCOPE}Guardrail`, + Effect: "Allow", + Action: "bedrock:InvokeGuardrailChecks", + Resource: "*", + }, + ); + } + + const lambdaArns = new Set(); + for (const interceptor of configuration.interceptorConfigurations ?? []) { + const interceptorConfiguration = interceptor.interceptor; + const interceptorArn = + interceptorConfiguration && + "lambda" in interceptorConfiguration && + interceptorConfiguration.lambda + ? interceptorConfiguration.lambda.arn + : undefined; + if (interceptorArn) lambdaArns.add(interceptorArn); + } + const transformArn = configuration.customTransformConfiguration?.lambda?.arn; + if (transformArn) lambdaArns.add(transformArn); + if (lambdaArns.size > 0) { + statements.push({ + Sid: `${GATEWAY_POLICY_SCOPE}Lambda`, + Effect: "Allow", + Action: "lambda:InvokeFunction", + Resource: [...lambdaArns].sort(), + }); + } + + return statements; +} + +function targetStatements( + configurations: GatewayTargetRoleConfiguration[], + context: GatewayTargetPolicyContext, +): PolicyStatement[] { + const lambdaArns = new Set(); + const s3ObjectArns = new Set(); + const apiGatewayArns = new Set(); + const runtimeArns = new Set(); + const knowledgeBaseArns = new Set(); + const oauthProviderArns = new Set(); + const apiKeyProviderArns = new Set(); + const secretArns = new Set(); + let usesWebSearch = false; + let usesAgenticRetrieve = false; + let usesBedrockMantle = false; + + for (const configuration of configurations) { + const target = configuration.targetConfiguration; + assertSupportedGatewayRoleTarget(configuration); + if (target && "mcp" in target && target.mcp) { + const mcp = target.mcp; + if ("lambda" in mcp && mcp.lambda) { + const lambdaArn = mcp.lambda.lambdaArn; + if (lambdaArn) { + lambdaArns.add(lambdaArn); + if (isUnqualifiedLambdaFunctionArn(lambdaArn)) { + lambdaArns.add(`${lambdaArn}:*`); + } + } + addSchemaS3Object(s3ObjectArns, mcp.lambda.toolSchema, context.partition); + } else if ("openApiSchema" in mcp && mcp.openApiSchema) { + addSchemaS3Object(s3ObjectArns, mcp.openApiSchema, context.partition); + } else if ("smithyModel" in mcp && mcp.smithyModel) { + addSchemaS3Object(s3ObjectArns, mcp.smithyModel, context.partition); + } else if ("mcpServer" in mcp && mcp.mcpServer) { + addSchemaS3Object(s3ObjectArns, mcp.mcpServer.mcpToolSchema, context.partition); + } else if ("apiGateway" in mcp && mcp.apiGateway) { + if (usesGatewayExecutionRole(configuration)) { + apiGatewayArns.add( + `arn:${context.partition}:execute-api:${context.region}:${context.accountId}:` + + `${mcp.apiGateway.restApiId}/${mcp.apiGateway.stage}/*/*`, + ); + } + } else if ("connector" in mcp && mcp.connector) { + const connectorId = mcp.connector.source?.connectorId; + if (connectorId === "web-search") { + usesWebSearch = usesWebSearch || usesGatewayExecutionRole(configuration); + } else if (connectorId === "bedrock-knowledge-bases") { + if (usesGatewayExecutionRole(configuration)) { + usesAgenticRetrieve = + addKnowledgeBaseConnectorPermissions( + knowledgeBaseArns, + mcp.connector.configurations, + context, + ) || usesAgenticRetrieve; + } + } + } + } else if (target && "http" in target && target.http) { + const http = target.http; + if ("agentcoreRuntime" in http && http.agentcoreRuntime) { + const runtimeArn = http.agentcoreRuntime.arn; + if (runtimeArn && usesGatewayExecutionRole(configuration)) { + runtimeArns.add(runtimeArn); + runtimeArns.add( + `${runtimeArn}/runtime-endpoint/${http.agentcoreRuntime.qualifier ?? "DEFAULT"}`, + ); + } + addSchemaS3Object(s3ObjectArns, http.agentcoreRuntime.schema?.source, context.partition); + } else if ("passthrough" in http && http.passthrough) { + addSchemaS3Object(s3ObjectArns, http.passthrough.schema?.source, context.partition); + } + } else if (target && "inference" in target && target.inference) { + usesBedrockMantle = + usesBedrockMantle || + (usesGatewayExecutionRole(configuration) && + (("connector" in target.inference && + target.inference.connector?.source?.connectorId === "bedrock-mantle") || + ("provider" in target.inference && + target.inference.provider?.endpoint?.includes("bedrock-mantle.") === true))); + } + + for (const providerConfiguration of configuration.credentialProviderConfigurations ?? []) { + const provider = providerConfiguration.credentialProvider; + if ( + providerConfiguration.credentialProviderType === "OAUTH" && + provider && + "oauthCredentialProvider" in provider && + provider.oauthCredentialProvider + ) { + if (provider.oauthCredentialProvider.providerArn) { + oauthProviderArns.add(provider.oauthCredentialProvider.providerArn); + } + } else if ( + providerConfiguration.credentialProviderType === "API_KEY" && + provider && + "apiKeyCredentialProvider" in provider && + provider.apiKeyCredentialProvider?.providerArn + ) { + apiKeyProviderArns.add(provider.apiKeyCredentialProvider.providerArn); + } + } + for (const secretArn of configuration.credentialProviderSecretArns ?? []) { + secretArns.add(secretArn); + } + } + + const statements: PolicyStatement[] = []; + addResourceStatement( + statements, + `${GATEWAY_TARGET_POLICY_SCOPE}Lambda`, + "lambda:InvokeFunction", + lambdaArns, + ); + addResourceStatement( + statements, + `${GATEWAY_TARGET_POLICY_SCOPE}S3Schema`, + "s3:GetObject", + s3ObjectArns, + ); + addResourceStatement( + statements, + `${GATEWAY_TARGET_POLICY_SCOPE}ApiGateway`, + "execute-api:Invoke", + apiGatewayArns, + ); + addResourceStatement( + statements, + `${GATEWAY_TARGET_POLICY_SCOPE}Runtime`, + "bedrock-agentcore:InvokeAgentRuntime", + runtimeArns, + ); + addResourceStatement( + statements, + `${GATEWAY_TARGET_POLICY_SCOPE}KnowledgeBase`, + ["bedrock:GetKnowledgeBase", "bedrock:Retrieve"], + knowledgeBaseArns, + ); + if (usesAgenticRetrieve) { + statements.push({ + Sid: `${GATEWAY_TARGET_POLICY_SCOPE}AgenticRetrieve`, + Effect: "Allow", + Action: "bedrock:AgenticRetrieveStream", + Resource: "*", + }); + } + if (usesBedrockMantle) { + statements.push({ + Sid: `${GATEWAY_TARGET_POLICY_SCOPE}BedrockMantle`, + Effect: "Allow", + Action: "bedrock-mantle:CreateInference", + Resource: "*", + }); + } + if (usesWebSearch) { + statements.push({ + Sid: `${GATEWAY_TARGET_POLICY_SCOPE}WebSearch`, + Effect: "Allow", + Action: "bedrock-agentcore:InvokeWebSearch", + Resource: `arn:${context.partition}:bedrock-agentcore:${context.region}:aws:tool/web-search.v1`, + }); + } + + const workloadIdentityArns = + oauthProviderArns.size > 0 || apiKeyProviderArns.size > 0 + ? workloadIdentityResources(context) + : []; + if (workloadIdentityArns.length > 0) { + statements.push({ + Sid: `${GATEWAY_TARGET_POLICY_SCOPE}WorkloadIdentity`, + Effect: "Allow", + Action: [ + "bedrock-agentcore:GetWorkloadAccessToken", + ...(oauthProviderArns.size > 0 + ? [ + "bedrock-agentcore:GetWorkloadAccessTokenForJWT", + "bedrock-agentcore:GetWorkloadAccessTokenForUserId", + ] + : []), + ].sort(), + Resource: workloadIdentityArns, + }); + } + if (oauthProviderArns.size > 0) { + const resources = tokenVaultResources(context, oauthProviderArns, workloadIdentityArns); + statements.push({ + Sid: `${GATEWAY_TARGET_POLICY_SCOPE}OAuthCompleteAuth`, + Effect: "Allow", + Action: "bedrock-agentcore:CompleteResourceTokenAuth", + Resource: resources, + }); + statements.push({ + Sid: `${GATEWAY_TARGET_POLICY_SCOPE}OAuth`, + Effect: "Allow", + Action: "bedrock-agentcore:GetResourceOauth2Token", + Resource: resources, + }); + } + if (apiKeyProviderArns.size > 0) { + statements.push({ + Sid: `${GATEWAY_TARGET_POLICY_SCOPE}ApiKey`, + Effect: "Allow", + Action: "bedrock-agentcore:GetResourceApiKey", + Resource: tokenVaultResources(context, apiKeyProviderArns, workloadIdentityArns), + }); + } + addResourceStatement( + statements, + `${GATEWAY_TARGET_POLICY_SCOPE}CredentialSecrets`, + "secretsmanager:GetSecretValue", + secretArns, + ); + + return statements; +} + +function workloadIdentityResources(context: GatewayTargetPolicyContext): string[] { + const workloadIdentityArn = context.workloadIdentityArn; + if (!workloadIdentityArn) { + throw new Error( + `Gateway "${context.gatewayId}" did not return a workload identity ARN required for Target credentials`, + ); + } + const marker = "/workload-identity/"; + const markerIndex = workloadIdentityArn.indexOf(marker); + if (markerIndex === -1) { + throw new Error(`Invalid Gateway workload identity ARN "${workloadIdentityArn}"`); + } + const directoryArn = workloadIdentityArn.slice(0, markerIndex); + return [directoryArn, workloadIdentityArn].sort(); +} + +function tokenVaultResources( + context: GatewayTargetPolicyContext, + providerArns: Set, + workloadIdentityArns: string[], +): string[] { + const tokenVaultArn = + `arn:${context.partition}:bedrock-agentcore:${context.region}:${context.accountId}:` + + "token-vault/default"; + return [...new Set([tokenVaultArn, ...providerArns, ...workloadIdentityArns])].sort(); +} + +function assertSupportedGatewayRoleTarget(configuration: GatewayTargetRoleConfiguration): void { + if (!usesGatewayExecutionRole(configuration)) return; + const target = configuration.targetConfiguration; + const supported = + Boolean(target && "mcp" in target && target.mcp && "lambda" in target.mcp) || + Boolean(target && "mcp" in target && target.mcp && "apiGateway" in target.mcp) || + Boolean( + target && + "mcp" in target && + target.mcp && + "connector" in target.mcp && + ["bedrock-knowledge-bases", "web-search"].includes( + target.mcp.connector?.source?.connectorId ?? "", + ), + ) || + Boolean(target && "http" in target && target.http && "agentcoreRuntime" in target.http) || + Boolean( + target && + "inference" in target && + target.inference && + (("connector" in target.inference && + target.inference.connector?.source?.connectorId === "bedrock-mantle") || + ("provider" in target.inference && + target.inference.provider?.endpoint?.includes("bedrock-mantle.") === true)), + ); + if (supported) return; + + throw new InputValidationError( + "The CLI cannot infer least-privilege IAM permissions for this Target's " + + "GATEWAY_IAM_ROLE credential; update the Gateway to use a customer-managed --role-arn", + ); +} + +function isUnqualifiedLambdaFunctionArn(arn: string): boolean { + return /^arn:[^:]+:lambda:[^:]+:\d{12}:function:[^:]+$/.test(arn); +} + +function addKnowledgeBaseConnectorPermissions( + resources: Set, + configurations: + | { + name?: string; + parameterValues?: unknown; + }[] + | undefined, + context: GatewayTargetPolicyContext, +): boolean { + let usesAgenticRetrieve = false; + for (const configuration of configurations ?? []) { + const parameterValues = asRecord(configuration.parameterValues); + if (configuration.name === "Retrieve") { + addKnowledgeBaseArn(resources, parameterValues?.knowledgeBaseId, context); + } else if (configuration.name === "AgenticRetrieveStream") { + usesAgenticRetrieve = true; + const retrievers = parameterValues?.retrievers; + if (!Array.isArray(retrievers)) continue; + for (const retriever of retrievers) { + const retrieverConfiguration = asRecord(asRecord(retriever)?.configuration); + const knowledgeBase = asRecord(retrieverConfiguration?.knowledgeBase); + addKnowledgeBaseArn(resources, knowledgeBase?.knowledgeBaseId, context); + } + } + } + return usesAgenticRetrieve; +} + +function addKnowledgeBaseArn( + resources: Set, + knowledgeBaseId: unknown, + context: GatewayTargetPolicyContext, +): void { + if (typeof knowledgeBaseId !== "string" || !knowledgeBaseId) return; + resources.add( + `arn:${context.partition}:bedrock:${context.region}:${context.accountId}:` + + `knowledge-base/${knowledgeBaseId}`, + ); +} + +function asRecord(value: unknown): Record | undefined { + if (!value || typeof value !== "object" || Array.isArray(value)) return undefined; + return value as Record; +} + +function addSchemaS3Object( + resources: Set, + schema: { s3?: { uri?: string } } | undefined, + partition: string, +): void { + const uri = schema?.s3?.uri; + if (!uri) return; + const match = /^s3:\/\/([^/]+)\/(.+)$/.exec(uri); + if (match) resources.add(`arn:${partition}:s3:::${match[1]}/${match[2]}`); +} + +function usesGatewayExecutionRole(configuration: GatewayTargetRoleConfiguration): boolean { + if (configuration.credentialProviderConfigurations !== undefined) { + return configuration.credentialProviderConfigurations.some( + ({ credentialProviderType }) => credentialProviderType === "GATEWAY_IAM_ROLE", + ); + } + + const target = configuration.targetConfiguration; + return Boolean( + target && + (("mcp" in target && + target.mcp && + ("lambda" in target.mcp || + "smithyModel" in target.mcp || + "apiGateway" in target.mcp || + ("connector" in target.mcp && + ["bedrock-knowledge-bases", "web-search"].includes( + target.mcp.connector?.source?.connectorId ?? "", + )))) || + ("http" in target && target.http && "agentcoreRuntime" in target.http) || + ("inference" in target && + target.inference && + (("connector" in target.inference && + target.inference.connector?.source?.connectorId === "bedrock-mantle") || + ("provider" in target.inference && + target.inference.provider?.endpoint?.includes("bedrock-mantle.") === true)))), + ); +} + +function addResourceStatement( + statements: PolicyStatement[], + sid: string, + action: string | string[], + resources: Set, +): void { + if (resources.size === 0) return; + statements.push({ + Sid: sid, + Effect: "Allow", + Action: action, + Resource: [...resources].sort(), + }); +} + +function policyDocumentForStatements(statements: PolicyStatement[]): string | undefined { + if (statements.length === 0) return undefined; + const document: PolicyDocument = { Version: "2012-10-17", Statement: statements }; + return JSON.stringify(document); +} diff --git a/src/core/index.tsx b/src/core/index.tsx index d3175502a..700432a98 100644 --- a/src/core/index.tsx +++ b/src/core/index.tsx @@ -101,7 +101,7 @@ export class CoreClient implements AwsClients { } // iam returns the IAM client for `config`, creating and caching it on first - // use (used to provision default harness execution roles). + // use (used to provision default execution roles). iam(config: ClientConfig): IAMClient { const key = cacheKey(config); let client = this.iamClients.get(key); diff --git a/src/handlers/gateway/create/index.tsx b/src/handlers/gateway/create/index.tsx new file mode 100644 index 000000000..7f22e3bde --- /dev/null +++ b/src/handlers/gateway/create/index.tsx @@ -0,0 +1,130 @@ +import type { + AuthorizerConfiguration, + GatewayInterceptorConfiguration, + GatewayProtocolConfiguration, +} from "@aws-sdk/client-bedrock-agentcore-control"; +import z from "zod"; +import { InputValidationError } from "../../../errors"; +import { createHandler, flag } from "../../../router"; +import { JsonRendererKey } from "../../../tui"; +import type { Core } from "../../types"; +import { coreOptsFromCtx, parseJsonArrayFlag, parseJsonObjectFlag, parseTags } from "../../utils"; +import { validateGatewayCreateInput } from "../mutations"; +import type { CreateGatewayInput } from "../types"; + +export const createCreateGatewayHandler = (core: Core) => + createHandler({ + name: "create", + description: "create an AgentCore Gateway", + flags: [ + flag("name", "the Gateway name", z.string().optional()), + flag( + "role-arn", + "IAM role the Gateway assumes; a least-privilege role is created when omitted", + z.string().optional(), + ), + flag( + "protocol", + "Gateway protocol: mcp or http (default: http)", + z.enum(["mcp", "http"]).optional(), + ), + flag( + "authorizer-type", + "inbound authorizer: AWS_IAM, CUSTOM_JWT, NONE, or AUTHENTICATE_ONLY", + z.enum(["AWS_IAM", "CUSTOM_JWT", "NONE", "AUTHENTICATE_ONLY"]).optional(), + ), + flag("description", "Gateway description", z.string().optional()), + flag( + "protocol-configuration", + "protocol configuration (JSON GatewayProtocolConfiguration)", + z.string().optional(), + ), + flag( + "authorizer-configuration", + "authorizer configuration (JSON AuthorizerConfiguration)", + z.string().optional(), + ), + flag("kms-key-arn", "KMS key ARN", z.string().optional()), + flag( + "interceptor-configurations", + "interceptor configurations (JSON GatewayInterceptorConfiguration[])", + z.string().optional(), + ), + flag("policy-engine-arn", "Policy Engine ARN", z.string().optional()), + flag( + "policy-engine-mode", + "Policy Engine mode: log-only or enforce", + z.enum(["log-only", "enforce"]).optional(), + ), + flag("exception-level", "exception detail level: debug", z.enum(["debug"]).optional()), + flag( + "tags", + "tags as key=value (repeatable) or a JSON object", + z.array(z.string()).optional(), + ), + flag("client-token", "idempotency token", z.string().optional()), + ], + handle: async (ctx, flags) => { + if (!flags.name) { + throw new InputValidationError("required option '--name ' not specified"); + } + if (!flags["authorizer-type"]) { + throw new InputValidationError( + "required option '--authorizer-type ' not specified", + ); + } + if ( + (flags["policy-engine-arn"] === undefined) !== + (flags["policy-engine-mode"] === undefined) + ) { + throw new InputValidationError( + "--policy-engine-arn and --policy-engine-mode must be supplied together", + ); + } + + const protocolConfiguration = parseJsonObjectFlag( + "protocol-configuration", + flags["protocol-configuration"], + ); + const authorizerConfiguration = parseJsonObjectFlag( + "authorizer-configuration", + flags["authorizer-configuration"], + ); + const interceptorConfigurations = parseJsonArrayFlag( + "interceptor-configurations", + flags["interceptor-configurations"], + ); + const policyEngineConfiguration = + flags["policy-engine-arn"] && flags["policy-engine-mode"] + ? { + arn: flags["policy-engine-arn"], + mode: + flags["policy-engine-mode"] === "enforce" + ? ("ENFORCE" as const) + : ("LOG_ONLY" as const), + } + : undefined; + const tags = parseTags(flags.tags); + + const input: CreateGatewayInput = { + name: flags.name, + ...(flags["role-arn"] ? { roleArn: flags["role-arn"] } : {}), + protocol: flags.protocol ?? "http", + authorizerType: flags["authorizer-type"], + ...(flags.description ? { description: flags.description } : {}), + ...(protocolConfiguration ? { protocolConfiguration } : {}), + ...(authorizerConfiguration ? { authorizerConfiguration } : {}), + ...(flags["kms-key-arn"] ? { kmsKeyArn: flags["kms-key-arn"] } : {}), + ...(interceptorConfigurations ? { interceptorConfigurations } : {}), + ...(policyEngineConfiguration ? { policyEngineConfiguration } : {}), + ...(flags["exception-level"] ? { exceptionLevel: "DEBUG" as const } : {}), + ...(tags ? { tags } : {}), + ...(flags["client-token"] ? { clientToken: flags["client-token"] } : {}), + }; + + validateGatewayCreateInput(input); + ctx + .require(JsonRendererKey) + .renderJson(await core.gateway.createGateway(input, coreOptsFromCtx(ctx))); + }, + }); diff --git a/src/handlers/gateway/gateway.create.test.tsx b/src/handlers/gateway/gateway.create.test.tsx new file mode 100644 index 000000000..979d82289 --- /dev/null +++ b/src/handlers/gateway/gateway.create.test.tsx @@ -0,0 +1,252 @@ +import { describe, expect, test } from "bun:test"; +import type { + CreateGatewayResponse, + CreateGatewayRuleResponse, + CreateGatewayTargetResponse, +} from "@aws-sdk/client-bedrock-agentcore-control"; +import { + createSilentLogger, + TestCoreClient, + TestGlobalConfigAccessor, + testIO, +} from "../../testing"; +import { createRootHandler } from "../index"; + +const REGION = "us-west-2"; +const GATEWAY_ID = "gateway-1"; + +async function run( + args: string[], + core = new TestCoreClient(), +): Promise<{ core: TestCoreClient; stdout: string }> { + const io = testIO(); + const root = createRootHandler(core, { + io: io.io, + logger: createSilentLogger(), + globalConfigAccessor: new TestGlobalConfigAccessor(), + }); + + await root.route(["node", "agentcore", ...args, "--region", REGION]); + return { core, stdout: io.stdout() }; +} + +describe("gateway create commands", () => { + test.each([ + ["mcp", "arn:aws:iam::123456789012:role/gateway"], + ["http", undefined], + ] as const)("creates a %s Gateway", async (protocol, roleArn) => { + const response = { + gatewayId: GATEWAY_ID, + name: "orders", + status: "CREATING", + } as CreateGatewayResponse; + const core = new TestCoreClient(); + core.gateway.setCreateResponse(response); + + const result = await run( + [ + "gateway", + "create", + "--name", + "orders", + ...(roleArn ? ["--role-arn", roleArn] : []), + "--protocol", + protocol, + "--authorizer-type", + "AWS_IAM", + "--tags", + "env=test", + "team=agentcore", + ], + core, + ); + + expect(core.gateway.calls).toEqual([ + { + method: "createGateway", + args: [ + { + name: "orders", + ...(roleArn ? { roleArn } : {}), + protocol, + authorizerType: "AWS_IAM", + tags: { env: "test", team: "agentcore" }, + }, + { region: REGION }, + ], + }, + ]); + expect(JSON.parse(result.stdout)).toEqual(response); + }); + + test("creates a guided MCP server Target", async () => { + const response = { + targetId: "target-1", + name: "calendar", + status: "CREATING", + } as CreateGatewayTargetResponse; + const core = new TestCoreClient(); + core.gateway.setCreateTargetResponse(response); + + const result = await run( + [ + "gateway", + "target", + "create", + "--gateway-id", + GATEWAY_ID, + "--name", + "calendar", + "--type", + "mcp-server", + "--endpoint", + "https://calendar.example.test/mcp", + ], + core, + ); + + expect(core.gateway.calls).toEqual([ + { + method: "createGatewayTarget", + args: [ + { + gatewayIdentifier: GATEWAY_ID, + name: "calendar", + targetConfiguration: { + mcp: { + mcpServer: { + endpoint: "https://calendar.example.test/mcp", + }, + }, + }, + }, + { region: REGION }, + ], + }, + ]); + expect(JSON.parse(result.stdout)).toEqual(response); + }); + + test("creates an exact Runtime Target without requiring a name", async () => { + const response = { targetId: "target-1", status: "CREATING" } as CreateGatewayTargetResponse; + const core = new TestCoreClient(); + core.gateway.setCreateTargetResponse(response); + const targetConfiguration = { + http: { + agentcoreRuntime: { + arn: "arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/runtime-1", + qualifier: "DEFAULT", + }, + }, + }; + + await run( + [ + "gateway", + "target", + "create", + "--gateway-id", + GATEWAY_ID, + "--target-configuration", + JSON.stringify(targetConfiguration), + ], + core, + ); + + expect(core.gateway.calls[0]).toEqual({ + method: "createGatewayTarget", + args: [ + { + gatewayIdentifier: GATEWAY_ID, + targetConfiguration, + }, + { region: REGION }, + ], + }); + }); + + test("creates a Rule with exact condition and action arrays", async () => { + const response = { + ruleId: "rule-1", + priority: 10, + status: "ACTIVE", + } as CreateGatewayRuleResponse; + const core = new TestCoreClient(); + core.gateway.setCreateRuleResponse(response); + const conditions = [{ matchPaths: { anyOf: ["/orders/*"] } }]; + const actions = [{ routeToTarget: { staticRoute: { targetName: "orders-api" } } }]; + + const result = await run( + [ + "gateway", + "rule", + "create", + "--gateway-id", + GATEWAY_ID, + "--priority", + "10", + "--conditions", + JSON.stringify(conditions), + "--actions", + JSON.stringify(actions), + ], + core, + ); + + expect(core.gateway.calls).toEqual([ + { + method: "createGatewayRule", + args: [ + { + gatewayIdentifier: GATEWAY_ID, + priority: 10, + conditions, + actions, + }, + { region: REGION }, + ], + }, + ]); + expect(JSON.parse(result.stdout)).toEqual(response); + }); +}); + +describe("gateway create validation", () => { + test.each([ + ["Gateway name", ["gateway", "create"], /--name/], + ["Gateway authorizer", ["gateway", "create", "--name", "orders"], /--authorizer-type/], + ["Target parent", ["gateway", "target", "create"], /--gateway-id/], + ["Rule parent", ["gateway", "rule", "create"], /--gateway-id/], + ] as const)("rejects missing %s input before calling Core", async (_name, args, error) => { + const core = new TestCoreClient(); + + await expect(run([...args], core)).rejects.toThrow(error); + expect(core.gateway.calls).toEqual([]); + }); + + test("rejects conflicting guided and exact Target input", async () => { + const core = new TestCoreClient(); + + await expect( + run( + [ + "gateway", + "target", + "create", + "--gateway-id", + GATEWAY_ID, + "--name", + "calendar", + "--type", + "mcp-server", + "--endpoint", + "https://calendar.example.test/mcp", + "--target-configuration", + '{"mcp":{"mcpServer":{"endpoint":"https://other.example.test/mcp"}}}', + ], + core, + ), + ).rejects.toThrow(/mutually exclusive/); + expect(core.gateway.calls).toEqual([]); + }); +}); diff --git a/src/handlers/gateway/gateway.test.tsx b/src/handlers/gateway/gateway.test.tsx index 9dc3ebe29..809eadc82 100644 --- a/src/handlers/gateway/gateway.test.tsx +++ b/src/handlers/gateway/gateway.test.tsx @@ -58,7 +58,7 @@ async function run( } describe("gateway command hierarchy", () => { - test("registers the Gateway read command hierarchy", () => { + test("registers the Gateway command hierarchy", () => { const root = createRootHandler(new TestCoreClient(), { io: testIO().io, logger: createSilentLogger(), @@ -70,13 +70,14 @@ describe("gateway command hierarchy", () => { expect(gateway?.flags().map((flag) => flag.name)).not.toContain("interactive"); expect(gateway?.children().map((child) => child.name())).toEqual([ + "create", "get", "list", "target", "rule", ]); - expect(target?.children().map((child) => child.name())).toEqual(["get", "list"]); - expect(rule?.children().map((child) => child.name())).toEqual(["get", "list"]); + expect(target?.children().map((child) => child.name())).toEqual(["create", "get", "list"]); + expect(rule?.children().map((child) => child.name())).toEqual(["create", "get", "list"]); }); test.each(["gateway", "gateway target", "gateway rule"])( diff --git a/src/handlers/gateway/index.tsx b/src/handlers/gateway/index.tsx index 0c431ca14..5a95cb96f 100644 --- a/src/handlers/gateway/index.tsx +++ b/src/handlers/gateway/index.tsx @@ -2,6 +2,7 @@ import type { AppIO } from "../../io"; import { Router } from "../../router"; import { createHelpDefault } from "../help"; import type { Core } from "../types"; +import { createCreateGatewayHandler } from "./create"; import { createGetGatewayHandler } from "./get"; import { createListGatewaysHandler } from "./list"; import { createGatewayRuleHandler } from "./rule"; @@ -10,6 +11,7 @@ import { createGatewayTargetHandler } from "./target"; export function createGatewayHandler(core: Core, io: AppIO): Router { return new Router("gateway", "inspect AgentCore Gateways") .default(createHelpDefault(io)) + .handler(createCreateGatewayHandler(core)) .handler(createGetGatewayHandler(core)) .handler(createListGatewaysHandler(core)) .handler(createGatewayTargetHandler(core, io)) diff --git a/src/handlers/gateway/mutations.ts b/src/handlers/gateway/mutations.ts new file mode 100644 index 000000000..dd1a34672 --- /dev/null +++ b/src/handlers/gateway/mutations.ts @@ -0,0 +1,102 @@ +import type { + CreateGatewayRequest, + TargetConfiguration, +} from "@aws-sdk/client-bedrock-agentcore-control"; +import { InputValidationError } from "../../errors"; +import type { + CreateGatewayInput, + CreateGatewayTargetInput, + ResolvedCreateGatewayInput, +} from "./types"; + +export function buildGatewayCreateRequest(input: ResolvedCreateGatewayInput): CreateGatewayRequest { + validateGatewayCreateInput(input); + const { protocol, ...request } = input; + + return { + ...request, + ...(protocol === "mcp" ? { protocolType: "MCP" as const } : {}), + }; +} + +export function validateGatewayCreateInput(input: CreateGatewayInput): CreateGatewayInput { + const { protocol, ...request } = input; + + if (!request.name) { + throw new InputValidationError("Gateway name is required"); + } + if (!request.authorizerType) { + throw new InputValidationError("Gateway authorizer type is required"); + } + if (request.authorizerType === "CUSTOM_JWT" && !request.authorizerConfiguration) { + throw new InputValidationError("CUSTOM_JWT requires --authorizer-configuration"); + } + if (request.authorizerType !== "CUSTOM_JWT" && request.authorizerConfiguration) { + throw new InputValidationError("--authorizer-configuration is valid only with CUSTOM_JWT"); + } + if (protocol === "http" && request.protocolConfiguration) { + throw new InputValidationError("--protocol-configuration is valid only with --protocol mcp"); + } + const policyEngine = request.policyEngineConfiguration; + if (policyEngine && (!policyEngine.arn || !policyEngine.mode)) { + throw new InputValidationError( + "Policy Engine attachment requires --policy-engine-arn and --policy-engine-mode", + ); + } + + return input; +} + +export function validateGatewayTargetCreateInput( + input: CreateGatewayTargetInput, +): CreateGatewayTargetInput { + if (!input.gatewayIdentifier) { + throw new InputValidationError("Gateway ID is required"); + } + if (!input.targetConfiguration) { + throw new InputValidationError("Target configuration is required"); + } + const variant = targetVariant(input.targetConfiguration); + if (!variant) { + throw new InputValidationError("Target configuration must use a known Target variant"); + } + if (!input.name && variant !== "http.agentcoreRuntime") { + throw new InputValidationError("Target name is required for non-Runtime targets"); + } + if ( + input.credentialProviderConfigurations && + input.credentialProviderConfigurations.length === 0 + ) { + throw new InputValidationError("Credential provider configurations cannot be empty"); + } + return input; +} + +export function targetVariant(configuration: TargetConfiguration): string | undefined { + if ("mcp" in configuration && configuration.mcp) { + return memberVariant("mcp", configuration.mcp, [ + "openApiSchema", + "smithyModel", + "lambda", + "mcpServer", + "apiGateway", + "connector", + ]); + } + if ("http" in configuration && configuration.http) { + return memberVariant("http", configuration.http, ["agentcoreRuntime", "passthrough"]); + } + if ("inference" in configuration && configuration.inference) { + return memberVariant("inference", configuration.inference, ["connector", "provider"]); + } + return undefined; +} + +function memberVariant( + outer: string, + value: object, + members: readonly string[], +): string | undefined { + const member = members.find((candidate) => candidate in value); + return member ? `${outer}.${member}` : undefined; +} diff --git a/src/handlers/gateway/rule/create/index.tsx b/src/handlers/gateway/rule/create/index.tsx new file mode 100644 index 000000000..296481b33 --- /dev/null +++ b/src/handlers/gateway/rule/create/index.tsx @@ -0,0 +1,51 @@ +import type { Action, Condition } from "@aws-sdk/client-bedrock-agentcore-control"; +import z from "zod"; +import { InputValidationError } from "../../../../errors"; +import { createHandler, flag } from "../../../../router"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx, parseJsonArrayFlag } from "../../../utils"; +import type { CreateGatewayRuleInput } from "../../types"; + +export const createCreateGatewayRuleHandler = (core: Core) => + createHandler({ + name: "create", + description: "create a Gateway Rule", + flags: [ + flag("gateway-id", "the parent Gateway ID", z.string().optional()), + flag( + "priority", + "Rule priority from 1 to 1000000", + z.number().int().min(1).max(1_000_000).optional(), + ), + flag("conditions", "Rule conditions (JSON Condition[])", z.string().optional()), + flag("actions", "Rule actions (JSON Action[])", z.string().optional()), + flag("description", "Rule description", z.string().optional()), + flag("client-token", "idempotency token", z.string().optional()), + ], + handle: async (ctx, flags) => { + if (!flags["gateway-id"]) { + throw new InputValidationError("required option '--gateway-id ' not specified"); + } + if (flags.priority === undefined) { + throw new InputValidationError("required option '--priority ' not specified"); + } + if (flags.actions === undefined) { + throw new InputValidationError("required option '--actions ' not specified"); + } + + const conditions = parseJsonArrayFlag("conditions", flags.conditions); + const actions = parseJsonArrayFlag("actions", flags.actions)!; + const input: CreateGatewayRuleInput = { + gatewayIdentifier: flags["gateway-id"], + priority: flags.priority, + actions, + ...(conditions !== undefined ? { conditions } : {}), + ...(flags.description ? { description: flags.description } : {}), + ...(flags["client-token"] ? { clientToken: flags["client-token"] } : {}), + }; + ctx + .require(JsonRendererKey) + .renderJson(await core.gateway.createGatewayRule(input, coreOptsFromCtx(ctx))); + }, + }); diff --git a/src/handlers/gateway/rule/index.tsx b/src/handlers/gateway/rule/index.tsx index 40ed75f68..a26daa91b 100644 --- a/src/handlers/gateway/rule/index.tsx +++ b/src/handlers/gateway/rule/index.tsx @@ -2,12 +2,14 @@ import type { AppIO } from "../../../io"; import { Router } from "../../../router"; import { createHelpDefault } from "../../help"; import type { Core } from "../../types"; +import { createCreateGatewayRuleHandler } from "./create"; import { createGetGatewayRuleHandler } from "./get"; import { createListGatewayRulesHandler } from "./list"; export function createGatewayRuleHandler(core: Core, io: AppIO): Router { return new Router("rule", "inspect rules for an AgentCore Gateway") .default(createHelpDefault(io)) + .handler(createCreateGatewayRuleHandler(core)) .handler(createGetGatewayRuleHandler(core)) .handler(createListGatewayRulesHandler(core)); } diff --git a/src/handlers/gateway/target/create/index.tsx b/src/handlers/gateway/target/create/index.tsx new file mode 100644 index 000000000..a90827310 --- /dev/null +++ b/src/handlers/gateway/target/create/index.tsx @@ -0,0 +1,112 @@ +import type { + CredentialProviderConfiguration, + MetadataConfiguration, + PrivateEndpoint, + TargetConfiguration, +} from "@aws-sdk/client-bedrock-agentcore-control"; +import z from "zod"; +import { InputValidationError } from "../../../../errors"; +import { createHandler, flag } from "../../../../router"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx, parseJsonArrayFlag, parseJsonObjectFlag } from "../../../utils"; +import { validateGatewayTargetCreateInput } from "../../mutations"; +import type { CreateGatewayTargetInput } from "../../types"; + +export const createCreateGatewayTargetHandler = (core: Core) => + createHandler({ + name: "create", + description: "create a Gateway Target", + flags: [ + flag("gateway-id", "the parent Gateway ID", z.string().optional()), + flag( + "name", + "Target name (optional only for an exact Runtime Target)", + z.string().optional(), + ), + flag("description", "Target description", z.string().optional()), + flag("type", "guided Target type: mcp-server", z.enum(["mcp-server"]).optional()), + flag("endpoint", "MCP server HTTPS endpoint", z.string().optional()), + flag( + "target-configuration", + "complete Target configuration (JSON TargetConfiguration)", + z.string().optional(), + ), + flag( + "credential-provider-configurations", + "outbound credentials (JSON CredentialProviderConfiguration[])", + z.string().optional(), + ), + flag( + "metadata-configuration", + "metadata propagation (JSON MetadataConfiguration)", + z.string().optional(), + ), + flag("private-endpoint", "private endpoint (JSON PrivateEndpoint)", z.string().optional()), + flag("client-token", "idempotency token", z.string().optional()), + ], + handle: async (ctx, flags) => { + if (!flags["gateway-id"]) { + throw new InputValidationError("required option '--gateway-id ' not specified"); + } + + const hasGuidedInput = flags.type !== undefined || flags.endpoint !== undefined; + if (flags["target-configuration"] !== undefined && hasGuidedInput) { + throw new InputValidationError( + "--target-configuration is mutually exclusive with --type and --endpoint", + ); + } + if (flags.type !== undefined && flags.endpoint === undefined) { + throw new InputValidationError("--type mcp-server requires --endpoint"); + } + if (flags.endpoint !== undefined && flags.type === undefined) { + throw new InputValidationError("--endpoint requires --type mcp-server"); + } + if (!hasGuidedInput && flags["target-configuration"] === undefined) { + throw new InputValidationError( + "either --type mcp-server with --endpoint or --target-configuration is required", + ); + } + + const exactConfiguration = parseJsonObjectFlag( + "target-configuration", + flags["target-configuration"], + ); + const targetConfiguration: TargetConfiguration = + exactConfiguration ?? + ({ + mcp: { + mcpServer: { + endpoint: flags.endpoint!, + }, + }, + } as TargetConfiguration); + const credentialProviderConfigurations = parseJsonArrayFlag( + "credential-provider-configurations", + flags["credential-provider-configurations"], + ); + const metadataConfiguration = parseJsonObjectFlag( + "metadata-configuration", + flags["metadata-configuration"], + ); + const privateEndpoint = parseJsonObjectFlag( + "private-endpoint", + flags["private-endpoint"], + ); + const input: CreateGatewayTargetInput = { + gatewayIdentifier: flags["gateway-id"], + targetConfiguration, + ...(flags.name ? { name: flags.name } : {}), + ...(flags.description ? { description: flags.description } : {}), + ...(credentialProviderConfigurations ? { credentialProviderConfigurations } : {}), + ...(metadataConfiguration ? { metadataConfiguration } : {}), + ...(privateEndpoint ? { privateEndpoint } : {}), + ...(flags["client-token"] ? { clientToken: flags["client-token"] } : {}), + }; + + validateGatewayTargetCreateInput(input); + ctx + .require(JsonRendererKey) + .renderJson(await core.gateway.createGatewayTarget(input, coreOptsFromCtx(ctx))); + }, + }); diff --git a/src/handlers/gateway/target/index.tsx b/src/handlers/gateway/target/index.tsx index 96f05dee9..898855bab 100644 --- a/src/handlers/gateway/target/index.tsx +++ b/src/handlers/gateway/target/index.tsx @@ -2,12 +2,14 @@ import type { AppIO } from "../../../io"; import { Router } from "../../../router"; import { createHelpDefault } from "../../help"; import type { Core } from "../../types"; +import { createCreateGatewayTargetHandler } from "./create"; import { createGetGatewayTargetHandler } from "./get"; import { createListGatewayTargetsHandler } from "./list"; export function createGatewayTargetHandler(core: Core, io: AppIO): Router { return new Router("target", "inspect targets for an AgentCore Gateway") .default(createHelpDefault(io)) + .handler(createCreateGatewayTargetHandler(core)) .handler(createGetGatewayTargetHandler(core)) .handler(createListGatewayTargetsHandler(core)); } diff --git a/src/handlers/gateway/types.tsx b/src/handlers/gateway/types.tsx index 0ed370130..436834e07 100644 --- a/src/handlers/gateway/types.tsx +++ b/src/handlers/gateway/types.tsx @@ -1,4 +1,10 @@ import type { + CreateGatewayRequest, + CreateGatewayResponse, + CreateGatewayRuleRequest, + CreateGatewayRuleResponse, + CreateGatewayTargetRequest, + CreateGatewayTargetResponse, GetGatewayResponse, GetGatewayRuleResponse, GetGatewayTargetResponse, @@ -8,7 +14,23 @@ import type { } from "@aws-sdk/client-bedrock-agentcore-control"; import type { CoreOptions } from "../../core/types"; +export type GatewayProtocol = "mcp" | "http"; + +export type CreateGatewayInput = Omit & { + roleArn?: string; + protocol: GatewayProtocol; +}; + +export type ResolvedCreateGatewayInput = CreateGatewayInput & { + roleArn: string; +}; + +export type CreateGatewayTargetInput = CreateGatewayTargetRequest; + +export type CreateGatewayRuleInput = CreateGatewayRuleRequest; + export interface CoreGatewayClient { + createGateway(input: CreateGatewayInput, options: CoreOptions): Promise; getGateway(id: string, options: CoreOptions): Promise; listGateways( nextToken: string | undefined, @@ -26,6 +48,10 @@ export interface CoreGatewayClient { maxResults: number | undefined, options: CoreOptions, ): Promise; + createGatewayTarget( + input: CreateGatewayTargetInput, + options: CoreOptions, + ): Promise; getGatewayRule( gatewayId: string, ruleId: string, @@ -37,4 +63,8 @@ export interface CoreGatewayClient { maxResults: number | undefined, options: CoreOptions, ): Promise; + createGatewayRule( + input: CreateGatewayRuleInput, + options: CoreOptions, + ): Promise; } diff --git a/src/handlers/utils.test.tsx b/src/handlers/utils.test.tsx index b9d5a384a..6b354f52b 100644 --- a/src/handlers/utils.test.tsx +++ b/src/handlers/utils.test.tsx @@ -1,5 +1,18 @@ import { describe, expect, test } from "bun:test"; -import { parseTags } from "./utils"; +import { parseJsonArrayFlag, parseJsonObjectFlag, parseTags } from "./utils"; + +describe("structured JSON flags", () => { + test("parses object and array values", () => { + expect(parseJsonObjectFlag("config", '{"enabled":true}')).toEqual({ enabled: true }); + expect(parseJsonArrayFlag("items", '[{"id":"a"}]')).toEqual([{ id: "a" }]); + }); + + test("rejects the wrong top-level shape", () => { + expect(() => parseJsonObjectFlag("config", "[]")).toThrow("must be a JSON object"); + expect(() => parseJsonObjectFlag("config", "null")).toThrow("must be a JSON object"); + expect(() => parseJsonArrayFlag("items", "{}")).toThrow("must be a JSON array"); + }); +}); describe("parseTags", () => { test("returns undefined for undefined input", () => { diff --git a/src/handlers/utils.tsx b/src/handlers/utils.tsx index 514f94e3c..737f02455 100644 --- a/src/handlers/utils.tsx +++ b/src/handlers/utils.tsx @@ -51,6 +51,27 @@ export function parseJsonFlagWithSchema( return result.data; } +export function parseJsonObjectFlag( + name: string, + raw: string | undefined, +): T | undefined { + const parsed = parseJsonFlag(name, raw); + if (parsed === undefined) return undefined; + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { + throw new InputValidationError(`Option '--${name}' must be a JSON object`); + } + return parsed as T; +} + +export function parseJsonArrayFlag(name: string, raw: string | undefined): T[] | undefined { + const parsed = parseJsonFlag(name, raw); + if (parsed === undefined) return undefined; + if (!Array.isArray(parsed)) { + throw new InputValidationError(`Option '--${name}' must be a JSON array`); + } + return parsed as T[]; +} + // parseTags parses a tags flag that accepts two mutually exclusive forms: // - Repeated key=value shorthand: ["env=prod", "team=foo"] // - A single JSON object: ['{"env":"prod","team":"foo"}'] diff --git a/src/testing/TestCoreClient.tsx b/src/testing/TestCoreClient.tsx index b85ed85d1..deb7e74ad 100644 --- a/src/testing/TestCoreClient.tsx +++ b/src/testing/TestCoreClient.tsx @@ -1,5 +1,8 @@ import type { CreateApiKeyCredentialProviderResponse, + CreateGatewayResponse, + CreateGatewayRuleResponse, + CreateGatewayTargetResponse, CreateHarnessEndpointRequest, CreateHarnessEndpointResponse, CreateHarnessResponse, @@ -64,7 +67,12 @@ import type { } from "@aws-sdk/client-bedrock-agentcore"; import type { Core } from "../handlers/types"; import type { CoreHarnessClient, CreateHarnessInput } from "../handlers/harness/types"; -import type { CoreGatewayClient } from "../handlers/gateway/types"; +import type { + CoreGatewayClient, + CreateGatewayInput, + CreateGatewayRuleInput, + CreateGatewayTargetInput, +} from "../handlers/gateway/types"; import type { CoreIdentityClient, CreateApiKeyCredentialProviderInput, @@ -141,10 +149,13 @@ const DEFAULT_GET_MEMORY_RECORD_RESPONSE: GetMemoryRecordOutput = { memoryRecord const DEFAULT_LIST_MEMORY_RECORDS_RESPONSE: ListMemoryRecordsOutput = { memoryRecordSummaries: [], }; +const DEFAULT_CREATE_GATEWAY_RESPONSE = {} as CreateGatewayResponse; const DEFAULT_GET_GATEWAY_RESPONSE = {} as GetGatewayResponse; const DEFAULT_LIST_GATEWAYS_RESPONSE: ListGatewaysResponse = { items: [] }; +const DEFAULT_CREATE_GATEWAY_TARGET_RESPONSE = {} as CreateGatewayTargetResponse; const DEFAULT_GET_GATEWAY_TARGET_RESPONSE = {} as GetGatewayTargetResponse; const DEFAULT_LIST_GATEWAY_TARGETS_RESPONSE: ListGatewayTargetsResponse = { items: [] }; +const DEFAULT_CREATE_GATEWAY_RULE_RESPONSE = {} as CreateGatewayRuleResponse; const DEFAULT_GET_GATEWAY_RULE_RESPONSE = {} as GetGatewayRuleResponse; const DEFAULT_LIST_GATEWAY_RULES_RESPONSE: ListGatewayRulesResponse = { gatewayRules: [] }; const DEFAULT_GET_RUNTIME_RESPONSE = {} as GetAgentRuntimeResponse; @@ -744,14 +755,23 @@ export class TestMemoryClient implements CoreMemoryClient { export class TestGatewayClient implements CoreGatewayClient { readonly calls: RecordedCall[] = []; + private createResponse: CreateGatewayResponse = DEFAULT_CREATE_GATEWAY_RESPONSE; private getResponse: GetGatewayResponse = DEFAULT_GET_GATEWAY_RESPONSE; private listResponses = new Map(); + private createTargetResponse: CreateGatewayTargetResponse = + DEFAULT_CREATE_GATEWAY_TARGET_RESPONSE; private getTargetResponse: GetGatewayTargetResponse = DEFAULT_GET_GATEWAY_TARGET_RESPONSE; private listTargetResponses = new Map(); + private createRuleResponse: CreateGatewayRuleResponse = DEFAULT_CREATE_GATEWAY_RULE_RESPONSE; private getRuleResponse: GetGatewayRuleResponse = DEFAULT_GET_GATEWAY_RULE_RESPONSE; private listRuleResponses = new Map(); private error?: Error; + setCreateResponse(response: CreateGatewayResponse): this { + this.createResponse = response; + return this; + } + setGetResponse(response: GetGatewayResponse): this { this.getResponse = response; return this; @@ -762,6 +782,11 @@ export class TestGatewayClient implements CoreGatewayClient { return this; } + setCreateTargetResponse(response: CreateGatewayTargetResponse): this { + this.createTargetResponse = response; + return this; + } + setGetTargetResponse(response: GetGatewayTargetResponse): this { this.getTargetResponse = response; return this; @@ -772,6 +797,11 @@ export class TestGatewayClient implements CoreGatewayClient { return this; } + setCreateRuleResponse(response: CreateGatewayRuleResponse): this { + this.createRuleResponse = response; + return this; + } + setGetRuleResponse(response: GetGatewayRuleResponse): this { this.getRuleResponse = response; return this; @@ -787,6 +817,15 @@ export class TestGatewayClient implements CoreGatewayClient { return this; } + async createGateway( + input: CreateGatewayInput, + options: CoreOptions, + ): Promise { + this.calls.push({ method: "createGateway", args: [input, options] }); + if (this.error) throw this.error; + return this.createResponse; + } + async getGateway(id: string, options: CoreOptions): Promise { this.calls.push({ method: "getGateway", args: [id, options] }); if (this.error) throw this.error; @@ -807,6 +846,15 @@ export class TestGatewayClient implements CoreGatewayClient { ); } + async createGatewayTarget( + input: CreateGatewayTargetInput, + options: CoreOptions, + ): Promise { + this.calls.push({ method: "createGatewayTarget", args: [input, options] }); + if (this.error) throw this.error; + return this.createTargetResponse; + } + async getGatewayTarget( gatewayId: string, targetId: string, @@ -835,6 +883,15 @@ export class TestGatewayClient implements CoreGatewayClient { ); } + async createGatewayRule( + input: CreateGatewayRuleInput, + options: CoreOptions, + ): Promise { + this.calls.push({ method: "createGatewayRule", args: [input, options] }); + if (this.error) throw this.error; + return this.createRuleResponse; + } + async getGatewayRule( gatewayId: string, ruleId: string,