diff --git a/README.md b/README.md index abfb0d46a..70a59a59b 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,15 @@ agentcore # interactive TUI ├── memory # inspect AgentCore Memories │ ├── get # fetch a Memory by id │ └── list # list Memories (server-side paginated) +├── gateway # inspect AgentCore Gateways +│ ├── get # get a Gateway by id +│ ├── list # list Gateways (server-side paginated) +│ ├── target +│ │ ├── get # get a Target under a Gateway +│ │ └── list # list Targets under a Gateway +│ └── rule +│ ├── get # get a Rule under a Gateway +│ └── list # list Rules under a Gateway ├── eval # evaluate and optimize AgentCore agents │ └── evaluator # manage AgentCore evaluators │ ├── llm-as-a-judge # LLM-as-a-Judge evaluators @@ -128,6 +137,14 @@ agentcore memory get --id agentcore memory get --id --view without_decryption agentcore memory list --max-results 20 +# Inspect Gateway resources without project configuration or deployment +agentcore gateway get --id +agentcore gateway list --max-results 20 +agentcore gateway target get --gateway-id --target-id +agentcore gateway target list --gateway-id --max-results 20 +agentcore gateway rule get --gateway-id --rule-id +agentcore gateway rule list --gateway-id --max-results 20 + # Manage API key credential providers agentcore identity api-key-credential-provider create --name my-provider --api-key agentcore identity api-key-credential-provider get --name my-provider diff --git a/src/components/RouterScreen.test.tsx b/src/components/RouterScreen.test.tsx index 9db945dd8..f28ee119d 100644 --- a/src/components/RouterScreen.test.tsx +++ b/src/components/RouterScreen.test.tsx @@ -19,6 +19,8 @@ describe("menu rendering", () => { expect(frame).toContain("inspect AgentCore Runtimes"); expect(frame).toContain("memory"); expect(frame).toContain("manage AgentCore Memories"); + expect(frame).toContain("gateway"); + expect(frame).toContain("inspect AgentCore Gateways"); expect(frame).toContain("config"); expect(frame).toContain("read/write global config values"); r.unmount(); diff --git a/src/core/core.test.ts b/src/core/core.test.ts index a8d54649c..6383a7171 100644 --- a/src/core/core.test.ts +++ b/src/core/core.test.ts @@ -191,6 +191,7 @@ test("exposes feature sub-clients", () => { }); expect(core.harness).toBeDefined(); expect(core.memory).toBeDefined(); + expect(core.gateway).toBeDefined(); }); test("getRuntime sends the abort signal to the control client", async () => { diff --git a/src/core/gateway.tsx b/src/core/gateway.tsx new file mode 100644 index 000000000..0e4808176 --- /dev/null +++ b/src/core/gateway.tsx @@ -0,0 +1,93 @@ +import { + GetGatewayCommand, + GetGatewayRuleCommand, + GetGatewayTargetCommand, + ListGatewayRulesCommand, + ListGatewaysCommand, + ListGatewayTargetsCommand, + type GetGatewayResponse, + type GetGatewayRuleResponse, + type GetGatewayTargetResponse, + type ListGatewayRulesResponse, + type ListGatewaysResponse, + type ListGatewayTargetsResponse, +} from "@aws-sdk/client-bedrock-agentcore-control"; +import type { CoreGatewayClient } from "../handlers/gateway/types"; +import type { AwsClients, CoreOptions } from "./types"; +import { toClientConfig } from "./utils"; + +export class GatewayClient implements CoreGatewayClient { + constructor(private readonly clients: AwsClients) {} + + async getGateway(id: string, options: CoreOptions): Promise { + return this.clients + .control(toClientConfig(options)) + .send(new GetGatewayCommand({ gatewayIdentifier: id })); + } + + async listGateways( + nextToken: string | undefined, + maxResults: number | undefined, + options: CoreOptions, + ): Promise { + return this.clients + .control(toClientConfig(options)) + .send(new ListGatewaysCommand({ nextToken, maxResults })); + } + + async getGatewayTarget( + gatewayId: string, + targetId: string, + options: CoreOptions, + ): Promise { + return this.clients.control(toClientConfig(options)).send( + new GetGatewayTargetCommand({ + gatewayIdentifier: gatewayId, + targetId, + }), + ); + } + + async listGatewayTargets( + gatewayId: string, + nextToken: string | undefined, + maxResults: number | undefined, + options: CoreOptions, + ): Promise { + return this.clients.control(toClientConfig(options)).send( + new ListGatewayTargetsCommand({ + gatewayIdentifier: gatewayId, + nextToken, + maxResults, + }), + ); + } + + async getGatewayRule( + gatewayId: string, + ruleId: string, + options: CoreOptions, + ): Promise { + return this.clients.control(toClientConfig(options)).send( + new GetGatewayRuleCommand({ + gatewayIdentifier: gatewayId, + ruleId, + }), + ); + } + + async listGatewayRules( + gatewayId: string, + nextToken: string | undefined, + maxResults: number | undefined, + options: CoreOptions, + ): Promise { + return this.clients.control(toClientConfig(options)).send( + new ListGatewayRulesCommand({ + gatewayIdentifier: gatewayId, + nextToken, + maxResults, + }), + ); + } +} diff --git a/src/core/index.tsx b/src/core/index.tsx index 77c617c17..d3175502a 100644 --- a/src/core/index.tsx +++ b/src/core/index.tsx @@ -2,6 +2,7 @@ import { BedrockAgentCoreControlClient } from "@aws-sdk/client-bedrock-agentcore import { BedrockAgentCoreClient } from "@aws-sdk/client-bedrock-agentcore"; import { IAMClient } from "@aws-sdk/client-iam"; import { EvalClient } from "./eval"; +import { GatewayClient } from "./gateway"; import { HarnessClient } from "./harness"; import { IdentityClient } from "./identity"; import { MemoryClient } from "./memory"; @@ -54,6 +55,7 @@ export class CoreClient implements AwsClients { readonly identity: IdentityClient = new IdentityClient(this); readonly memory: MemoryClient = new MemoryClient(this); readonly runtime: RuntimeClient; + readonly gateway: GatewayClient = new GatewayClient(this); readonly eval: EvalClient = new EvalClient(this); readonly projectManager: ProjectManager; diff --git a/src/handlers/gateway/__fixtures__/GetGatewayCommand.4216a59651bb046a.json b/src/handlers/gateway/__fixtures__/GetGatewayCommand.4216a59651bb046a.json new file mode 100644 index 000000000..1007a1d2f --- /dev/null +++ b/src/handlers/gateway/__fixtures__/GetGatewayCommand.4216a59651bb046a.json @@ -0,0 +1,20 @@ +{ + "gatewayArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:gateway/agentcore-cli-gateway-read-fixture-a-l6opkbe2kd", + "gatewayId": "agentcore-cli-gateway-read-fixture-a-l6opkbe2kd", + "createdAt": { + "$date": "2026-07-29T22:19:37.409Z" + }, + "updatedAt": { + "$date": "2026-07-29T22:19:37.971Z" + }, + "status": "READY", + "name": "agentcore-cli-gateway-read-fixture-a", + "authorizerType": "NONE", + "gatewayUrl": "https://agentcore-cli-gateway-read-fixture-a-l6opkbe2kd.gateway.bedrock-agentcore.us-west-2.amazonaws.com/mcp", + "description": "AgentCore CLI persistent Gateway read fixture", + "roleArn": "arn:aws:iam::685197708687:role/AgentCoreCliGatewayReadFixtureRole", + "protocolType": "MCP", + "workloadIdentityDetails": { + "workloadIdentityArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:workload-identity-directory/default/workload-identity/agentcore-cli-gateway-read-fixture-a-l6opkbe2kd" + } +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/GetGatewayCommand.fe70727e83c9acf1.json b/src/handlers/gateway/__fixtures__/GetGatewayCommand.fe70727e83c9acf1.json new file mode 100644 index 000000000..09cf9d0d7 --- /dev/null +++ b/src/handlers/gateway/__fixtures__/GetGatewayCommand.fe70727e83c9acf1.json @@ -0,0 +1,6 @@ +{ + "$error": { + "name": "ResourceNotFoundException", + "message": "Failed to retrieve gateway because it doesn't exist. Retry the request with a different resource identifier." + } +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/GetGatewayRuleCommand.137147fc065772f4.json b/src/handlers/gateway/__fixtures__/GetGatewayRuleCommand.137147fc065772f4.json new file mode 100644 index 000000000..0fe0cb07a --- /dev/null +++ b/src/handlers/gateway/__fixtures__/GetGatewayRuleCommand.137147fc065772f4.json @@ -0,0 +1,6 @@ +{ + "$error": { + "name": "ResourceNotFoundException", + "message": "GatewayRule 00000000-0000-4000-8000-000000000000 not found" + } +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/GetGatewayRuleCommand.e2b99534dc4a233f.json b/src/handlers/gateway/__fixtures__/GetGatewayRuleCommand.e2b99534dc4a233f.json new file mode 100644 index 000000000..6d55027d8 --- /dev/null +++ b/src/handlers/gateway/__fixtures__/GetGatewayRuleCommand.e2b99534dc4a233f.json @@ -0,0 +1,31 @@ +{ + "ruleId": "d396c3f4-4591-41b3-a4d5-816e03c32419", + "gatewayArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:gateway/agentcore-cli-gateway-read-rule-fixture-lhpid2reoy", + "priority": 10, + "actions": [ + { + "routeToTarget": { + "staticRoute": { + "targetName": "agentcore-cli-gateway-read-http-target" + } + } + } + ], + "createdAt": { + "$date": "2026-07-30T00:13:42.602Z" + }, + "status": "ACTIVE", + "conditions": [ + { + "matchPaths": { + "anyOf": [ + "/agentcore-cli-gateway-read-http-target/*" + ] + } + } + ], + "description": "AgentCore CLI persistent Gateway Rule read fixture A1", + "updatedAt": { + "$date": "2026-07-30T00:13:42.602Z" + } +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/GetGatewayTargetCommand.212e9693cb8e4550.json b/src/handlers/gateway/__fixtures__/GetGatewayTargetCommand.212e9693cb8e4550.json new file mode 100644 index 000000000..8420b351e --- /dev/null +++ b/src/handlers/gateway/__fixtures__/GetGatewayTargetCommand.212e9693cb8e4550.json @@ -0,0 +1,6 @@ +{ + "$error": { + "name": "ResourceNotFoundException", + "message": "Failed to retrieve target because it doesn't exist. Retry the request with a different resource identifier." + } +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/GetGatewayTargetCommand.d547517995d9266e.json b/src/handlers/gateway/__fixtures__/GetGatewayTargetCommand.d547517995d9266e.json new file mode 100644 index 000000000..d66b68669 --- /dev/null +++ b/src/handlers/gateway/__fixtures__/GetGatewayTargetCommand.d547517995d9266e.json @@ -0,0 +1,23 @@ +{ + "gatewayArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:gateway/agentcore-cli-gateway-read-fixture-a-l6opkbe2kd", + "targetId": "KALJACI9HO", + "createdAt": { + "$date": "2026-07-29T22:20:22.462Z" + }, + "updatedAt": { + "$date": "2026-07-29T22:20:25.484Z" + }, + "status": "READY", + "name": "agentcore-cli-gateway-read-target-a1", + "targetConfiguration": { + "mcp": { + "mcpServer": { + "endpoint": "https://mcp.context7.com/mcp" + } + } + }, + "description": "AgentCore CLI persistent Gateway Target read fixture", + "lastSynchronizedAt": { + "$date": "2026-07-29T22:20:25.286Z" + } +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/ListGatewayRulesCommand.11f078b3d9015781.json b/src/handlers/gateway/__fixtures__/ListGatewayRulesCommand.11f078b3d9015781.json new file mode 100644 index 000000000..e04644374 --- /dev/null +++ b/src/handlers/gateway/__fixtures__/ListGatewayRulesCommand.11f078b3d9015781.json @@ -0,0 +1,35 @@ +{ + "gatewayRules": [ + { + "ruleId": "545ee264-f3e0-4333-ad31-0a260d0c0a7a", + "gatewayArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:gateway/agentcore-cli-gateway-read-rule-fixture-lhpid2reoy", + "priority": 20, + "actions": [ + { + "routeToTarget": { + "staticRoute": { + "targetName": "agentcore-cli-gateway-read-http-target" + } + } + } + ], + "createdAt": { + "$date": "2026-07-30T00:13:42.856Z" + }, + "status": "ACTIVE", + "conditions": [ + { + "matchPaths": { + "anyOf": [ + "/agentcore-cli-gateway-read-http-target/*" + ] + } + } + ], + "description": "AgentCore CLI persistent Gateway Rule read fixture A2", + "updatedAt": { + "$date": "2026-07-30T00:13:42.856Z" + } + } + ] +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/ListGatewayRulesCommand.aea3eebbdd6782a6.json b/src/handlers/gateway/__fixtures__/ListGatewayRulesCommand.aea3eebbdd6782a6.json new file mode 100644 index 000000000..3ed7cc8c2 --- /dev/null +++ b/src/handlers/gateway/__fixtures__/ListGatewayRulesCommand.aea3eebbdd6782a6.json @@ -0,0 +1,36 @@ +{ + "gatewayRules": [ + { + "ruleId": "d396c3f4-4591-41b3-a4d5-816e03c32419", + "gatewayArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:gateway/agentcore-cli-gateway-read-rule-fixture-lhpid2reoy", + "priority": 10, + "actions": [ + { + "routeToTarget": { + "staticRoute": { + "targetName": "agentcore-cli-gateway-read-http-target" + } + } + } + ], + "createdAt": { + "$date": "2026-07-30T00:13:42.602Z" + }, + "status": "ACTIVE", + "conditions": [ + { + "matchPaths": { + "anyOf": [ + "/agentcore-cli-gateway-read-http-target/*" + ] + } + } + ], + "description": "AgentCore CLI persistent Gateway Rule read fixture A1", + "updatedAt": { + "$date": "2026-07-30T00:13:42.602Z" + } + } + ], + "nextToken": "{\"qualified_gateway_id\":\"685197708687/agentcore-cli-gateway-read-rule-fixture-lhpid2reoy/default\",\"sort_key\":\"PRIORITY#0000000010000\"}" +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/ListGatewayTargetsCommand.7eb952b3b7e51b55.json b/src/handlers/gateway/__fixtures__/ListGatewayTargetsCommand.7eb952b3b7e51b55.json new file mode 100644 index 000000000..c5682ea77 --- /dev/null +++ b/src/handlers/gateway/__fixtures__/ListGatewayTargetsCommand.7eb952b3b7e51b55.json @@ -0,0 +1,21 @@ +{ + "items": [ + { + "targetId": "KALJACI9HO", + "name": "agentcore-cli-gateway-read-target-a1", + "status": "READY", + "createdAt": { + "$date": "2026-07-29T22:20:22.462Z" + }, + "updatedAt": { + "$date": "2026-07-29T22:20:25.484Z" + }, + "description": "AgentCore CLI persistent Gateway Target read fixture", + "lastSynchronizedAt": { + "$date": "2026-07-29T22:20:25.286Z" + }, + "targetType": "MCP_SERVER" + } + ], + "nextToken": "AQICAHjoR4+rRMkqrOqAClU4WXRl/BYNcFGMZzyZmTV8TePbugFWJqzdVKbd+myGJJE2QPzmAAABPjCCAToGCSqGSIb3DQEHBqCCASswggEnAgEAMIIBIAYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAxlGRbqXmW0GWjjpXQCARCAgfJloMkFwZZZ2tYdIS8tEp+oLT2WKDuRe9YKoYsiePXCXtW0HZ0DjtknJUqWnL7Jy3LrAvV0vMhSGYGjFyAwzRMpV+5o2gp8SGOZ0TW9u9+n9Mi8AidSRpGsiww/ZtISfm4ltKUo48nsnWTheeTstx28BOX9wUANsIXKIuFobJtbBzRrTBPQfE7kO3A5xBKTu44mg5l3t5aCnq6SYNAXiVEBtc0uw8i6XGOp+rh7Ux5mMeYJvRj0tSeHRmMu2nq+T9s+fv35h4yaMXyCn9un7JH+RhlnEwLuJklDOIMnYLEGmGM6o96+WvWvVsb0uufyVUVg7A==" +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/ListGatewayTargetsCommand.bfef00ff066f3fe6.json b/src/handlers/gateway/__fixtures__/ListGatewayTargetsCommand.bfef00ff066f3fe6.json new file mode 100644 index 000000000..550f89c3c --- /dev/null +++ b/src/handlers/gateway/__fixtures__/ListGatewayTargetsCommand.bfef00ff066f3fe6.json @@ -0,0 +1,20 @@ +{ + "items": [ + { + "targetId": "ZZHBZX71VQ", + "name": "agentcore-cli-gateway-read-target-a2", + "status": "READY", + "createdAt": { + "$date": "2026-07-29T22:20:25.993Z" + }, + "updatedAt": { + "$date": "2026-07-29T22:20:29.823Z" + }, + "description": "AgentCore CLI persistent Gateway Target read fixture", + "lastSynchronizedAt": { + "$date": "2026-07-29T22:20:29.628Z" + }, + "targetType": "MCP_SERVER" + } + ] +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/ListGatewaysCommand.6a0898242458b177.json b/src/handlers/gateway/__fixtures__/ListGatewaysCommand.6a0898242458b177.json new file mode 100644 index 000000000..6affcb3e0 --- /dev/null +++ b/src/handlers/gateway/__fixtures__/ListGatewaysCommand.6a0898242458b177.json @@ -0,0 +1,19 @@ +{ + "items": [ + { + "gatewayId": "agentcore-cli-gateway-read-fixture-b-l4suujplgc", + "name": "agentcore-cli-gateway-read-fixture-b", + "status": "READY", + "createdAt": { + "$date": "2026-07-29T22:19:40.838Z" + }, + "updatedAt": { + "$date": "2026-07-29T22:19:41.416Z" + }, + "authorizerType": "NONE", + "description": "AgentCore CLI persistent Gateway read fixture", + "protocolType": "MCP" + } + ], + "nextToken": "AQICAHjoR4+rRMkqrOqAClU4WXRl/BYNcFGMZzyZmTV8TePbugFgEU2Zff6U7uPpIzVl3mpcAAABNjCCATIGCSqGSIb3DQEHBqCCASMwggEfAgEAMIIBGAYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAy44tTEe9iQm5g4TEICARCAgeoYkCmeUuPyXdGoQYo/zCK1lbReI/Je6RZ5Cr8gvnP++jvkOO3r/xi/0pIYhCdasgTBKRE7Aquwo6rI9dtjcVxYM1rOTrwBlJlBL4LHgGhvZ3V+PZqEnZhYJhFSzJul3zHtt2pBR5TK3dvfvNIYF86HkgK1+9aw989Yc8hbRmJw2UW1NqDyKepvkcANA7g7oOZ7RjGmCv6t8s+7WEK+10ru8MFw3Jhh1aNaQiC+mioShCF9ad/JxQro/5Ndd1Hs2edMqKcWdFSRuvmBfTRGp5URdFYrOSf7FseWFfTyxFEQHJCROFLcVsruMAA=" +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/ListGatewaysCommand.7d2e22c637f6b633.json b/src/handlers/gateway/__fixtures__/ListGatewaysCommand.7d2e22c637f6b633.json new file mode 100644 index 000000000..e706d8667 --- /dev/null +++ b/src/handlers/gateway/__fixtures__/ListGatewaysCommand.7d2e22c637f6b633.json @@ -0,0 +1,19 @@ +{ + "items": [ + { + "gatewayId": "agentcore-cli-gateway-read-fixture-a-l6opkbe2kd", + "name": "agentcore-cli-gateway-read-fixture-a", + "status": "READY", + "createdAt": { + "$date": "2026-07-29T22:19:37.409Z" + }, + "updatedAt": { + "$date": "2026-07-29T22:19:37.971Z" + }, + "authorizerType": "NONE", + "description": "AgentCore CLI persistent Gateway read fixture", + "protocolType": "MCP" + } + ], + "nextToken": "AQICAHjoR4+rRMkqrOqAClU4WXRl/BYNcFGMZzyZmTV8TePbugGmDfGSBe8kzNwZeGb6DODeAAABNjCCATIGCSqGSIb3DQEHBqCCASMwggEfAgEAMIIBGAYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAzdQYXAO15etDj2KCQCARCAgeoeDew6wu8oPGbpoRKbfiRPkzFTSR2EsXeegt/eIh0+hts15SSl5Qe9uqYZIO2Mqayjnna9DGOiUn3zT9p26QZBF6hACdM/9sBc+I455N+mYlsMbSyGKqNAKbkBpmk87Mepycnevr0H0gQSxRyhI59zBUOIgzlr0aVOlIvMAU6xrj4r6cu5QAuP70Jt8PNN74bcJ6rDeM5CB9ROkY8NHULN+5x0Lch1ON6Bb93lnJml5AquYm/2aVBExyYwqO41RHmffmJj6N3hBTrIicQ236D+BrMfghpr7s3rmDZvozXDmWCdDmMFutng3M0=" +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/get.golden.json b/src/handlers/gateway/__fixtures__/get.golden.json new file mode 100644 index 000000000..0a67c3aa9 --- /dev/null +++ b/src/handlers/gateway/__fixtures__/get.golden.json @@ -0,0 +1,16 @@ +{ + "gatewayArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:gateway/agentcore-cli-gateway-read-fixture-a-l6opkbe2kd", + "gatewayId": "agentcore-cli-gateway-read-fixture-a-l6opkbe2kd", + "createdAt": "2026-07-29T22:19:37.409Z", + "updatedAt": "2026-07-29T22:19:37.971Z", + "status": "READY", + "name": "agentcore-cli-gateway-read-fixture-a", + "authorizerType": "NONE", + "gatewayUrl": "https://agentcore-cli-gateway-read-fixture-a-l6opkbe2kd.gateway.bedrock-agentcore.us-west-2.amazonaws.com/mcp", + "description": "AgentCore CLI persistent Gateway read fixture", + "roleArn": "arn:aws:iam::685197708687:role/AgentCoreCliGatewayReadFixtureRole", + "protocolType": "MCP", + "workloadIdentityDetails": { + "workloadIdentityArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:workload-identity-directory/default/workload-identity/agentcore-cli-gateway-read-fixture-a-l6opkbe2kd" + } +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/list-page-1.golden.json b/src/handlers/gateway/__fixtures__/list-page-1.golden.json new file mode 100644 index 000000000..639ce9b51 --- /dev/null +++ b/src/handlers/gateway/__fixtures__/list-page-1.golden.json @@ -0,0 +1,15 @@ +{ + "items": [ + { + "gatewayId": "agentcore-cli-gateway-read-fixture-a-l6opkbe2kd", + "name": "agentcore-cli-gateway-read-fixture-a", + "status": "READY", + "createdAt": "2026-07-29T22:19:37.409Z", + "updatedAt": "2026-07-29T22:19:37.971Z", + "authorizerType": "NONE", + "description": "AgentCore CLI persistent Gateway read fixture", + "protocolType": "MCP" + } + ], + "nextToken": "AQICAHjoR4+rRMkqrOqAClU4WXRl/BYNcFGMZzyZmTV8TePbugGmDfGSBe8kzNwZeGb6DODeAAABNjCCATIGCSqGSIb3DQEHBqCCASMwggEfAgEAMIIBGAYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAzdQYXAO15etDj2KCQCARCAgeoeDew6wu8oPGbpoRKbfiRPkzFTSR2EsXeegt/eIh0+hts15SSl5Qe9uqYZIO2Mqayjnna9DGOiUn3zT9p26QZBF6hACdM/9sBc+I455N+mYlsMbSyGKqNAKbkBpmk87Mepycnevr0H0gQSxRyhI59zBUOIgzlr0aVOlIvMAU6xrj4r6cu5QAuP70Jt8PNN74bcJ6rDeM5CB9ROkY8NHULN+5x0Lch1ON6Bb93lnJml5AquYm/2aVBExyYwqO41RHmffmJj6N3hBTrIicQ236D+BrMfghpr7s3rmDZvozXDmWCdDmMFutng3M0=" +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/list-page-2.golden.json b/src/handlers/gateway/__fixtures__/list-page-2.golden.json new file mode 100644 index 000000000..44b8eda17 --- /dev/null +++ b/src/handlers/gateway/__fixtures__/list-page-2.golden.json @@ -0,0 +1,15 @@ +{ + "items": [ + { + "gatewayId": "agentcore-cli-gateway-read-fixture-b-l4suujplgc", + "name": "agentcore-cli-gateway-read-fixture-b", + "status": "READY", + "createdAt": "2026-07-29T22:19:40.838Z", + "updatedAt": "2026-07-29T22:19:41.416Z", + "authorizerType": "NONE", + "description": "AgentCore CLI persistent Gateway read fixture", + "protocolType": "MCP" + } + ], + "nextToken": "AQICAHjoR4+rRMkqrOqAClU4WXRl/BYNcFGMZzyZmTV8TePbugFgEU2Zff6U7uPpIzVl3mpcAAABNjCCATIGCSqGSIb3DQEHBqCCASMwggEfAgEAMIIBGAYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAy44tTEe9iQm5g4TEICARCAgeoYkCmeUuPyXdGoQYo/zCK1lbReI/Je6RZ5Cr8gvnP++jvkOO3r/xi/0pIYhCdasgTBKRE7Aquwo6rI9dtjcVxYM1rOTrwBlJlBL4LHgGhvZ3V+PZqEnZhYJhFSzJul3zHtt2pBR5TK3dvfvNIYF86HkgK1+9aw989Yc8hbRmJw2UW1NqDyKepvkcANA7g7oOZ7RjGmCv6t8s+7WEK+10ru8MFw3Jhh1aNaQiC+mioShCF9ad/JxQro/5Ndd1Hs2edMqKcWdFSRuvmBfTRGp5URdFYrOSf7FseWFfTyxFEQHJCROFLcVsruMAA=" +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/rule-get.golden.json b/src/handlers/gateway/__fixtures__/rule-get.golden.json new file mode 100644 index 000000000..cfa26cf3e --- /dev/null +++ b/src/handlers/gateway/__fixtures__/rule-get.golden.json @@ -0,0 +1,27 @@ +{ + "ruleId": "d396c3f4-4591-41b3-a4d5-816e03c32419", + "gatewayArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:gateway/agentcore-cli-gateway-read-rule-fixture-lhpid2reoy", + "priority": 10, + "actions": [ + { + "routeToTarget": { + "staticRoute": { + "targetName": "agentcore-cli-gateway-read-http-target" + } + } + } + ], + "createdAt": "2026-07-30T00:13:42.602Z", + "status": "ACTIVE", + "conditions": [ + { + "matchPaths": { + "anyOf": [ + "/agentcore-cli-gateway-read-http-target/*" + ] + } + } + ], + "description": "AgentCore CLI persistent Gateway Rule read fixture A1", + "updatedAt": "2026-07-30T00:13:42.602Z" +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/rule-list-page-1.golden.json b/src/handlers/gateway/__fixtures__/rule-list-page-1.golden.json new file mode 100644 index 000000000..eae552a23 --- /dev/null +++ b/src/handlers/gateway/__fixtures__/rule-list-page-1.golden.json @@ -0,0 +1,32 @@ +{ + "gatewayRules": [ + { + "ruleId": "d396c3f4-4591-41b3-a4d5-816e03c32419", + "gatewayArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:gateway/agentcore-cli-gateway-read-rule-fixture-lhpid2reoy", + "priority": 10, + "actions": [ + { + "routeToTarget": { + "staticRoute": { + "targetName": "agentcore-cli-gateway-read-http-target" + } + } + } + ], + "createdAt": "2026-07-30T00:13:42.602Z", + "status": "ACTIVE", + "conditions": [ + { + "matchPaths": { + "anyOf": [ + "/agentcore-cli-gateway-read-http-target/*" + ] + } + } + ], + "description": "AgentCore CLI persistent Gateway Rule read fixture A1", + "updatedAt": "2026-07-30T00:13:42.602Z" + } + ], + "nextToken": "{\"qualified_gateway_id\":\"685197708687/agentcore-cli-gateway-read-rule-fixture-lhpid2reoy/default\",\"sort_key\":\"PRIORITY#0000000010000\"}" +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/rule-list-page-2.golden.json b/src/handlers/gateway/__fixtures__/rule-list-page-2.golden.json new file mode 100644 index 000000000..8b5065599 --- /dev/null +++ b/src/handlers/gateway/__fixtures__/rule-list-page-2.golden.json @@ -0,0 +1,31 @@ +{ + "gatewayRules": [ + { + "ruleId": "545ee264-f3e0-4333-ad31-0a260d0c0a7a", + "gatewayArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:gateway/agentcore-cli-gateway-read-rule-fixture-lhpid2reoy", + "priority": 20, + "actions": [ + { + "routeToTarget": { + "staticRoute": { + "targetName": "agentcore-cli-gateway-read-http-target" + } + } + } + ], + "createdAt": "2026-07-30T00:13:42.856Z", + "status": "ACTIVE", + "conditions": [ + { + "matchPaths": { + "anyOf": [ + "/agentcore-cli-gateway-read-http-target/*" + ] + } + } + ], + "description": "AgentCore CLI persistent Gateway Rule read fixture A2", + "updatedAt": "2026-07-30T00:13:42.856Z" + } + ] +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/target-get.golden.json b/src/handlers/gateway/__fixtures__/target-get.golden.json new file mode 100644 index 000000000..18b42043f --- /dev/null +++ b/src/handlers/gateway/__fixtures__/target-get.golden.json @@ -0,0 +1,17 @@ +{ + "gatewayArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:gateway/agentcore-cli-gateway-read-fixture-a-l6opkbe2kd", + "targetId": "KALJACI9HO", + "createdAt": "2026-07-29T22:20:22.462Z", + "updatedAt": "2026-07-29T22:20:25.484Z", + "status": "READY", + "name": "agentcore-cli-gateway-read-target-a1", + "targetConfiguration": { + "mcp": { + "mcpServer": { + "endpoint": "https://mcp.context7.com/mcp" + } + } + }, + "description": "AgentCore CLI persistent Gateway Target read fixture", + "lastSynchronizedAt": "2026-07-29T22:20:25.286Z" +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/target-list-page-1.golden.json b/src/handlers/gateway/__fixtures__/target-list-page-1.golden.json new file mode 100644 index 000000000..e1a0b829c --- /dev/null +++ b/src/handlers/gateway/__fixtures__/target-list-page-1.golden.json @@ -0,0 +1,15 @@ +{ + "items": [ + { + "targetId": "KALJACI9HO", + "name": "agentcore-cli-gateway-read-target-a1", + "status": "READY", + "createdAt": "2026-07-29T22:20:22.462Z", + "updatedAt": "2026-07-29T22:20:25.484Z", + "description": "AgentCore CLI persistent Gateway Target read fixture", + "lastSynchronizedAt": "2026-07-29T22:20:25.286Z", + "targetType": "MCP_SERVER" + } + ], + "nextToken": "AQICAHjoR4+rRMkqrOqAClU4WXRl/BYNcFGMZzyZmTV8TePbugFWJqzdVKbd+myGJJE2QPzmAAABPjCCAToGCSqGSIb3DQEHBqCCASswggEnAgEAMIIBIAYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAxlGRbqXmW0GWjjpXQCARCAgfJloMkFwZZZ2tYdIS8tEp+oLT2WKDuRe9YKoYsiePXCXtW0HZ0DjtknJUqWnL7Jy3LrAvV0vMhSGYGjFyAwzRMpV+5o2gp8SGOZ0TW9u9+n9Mi8AidSRpGsiww/ZtISfm4ltKUo48nsnWTheeTstx28BOX9wUANsIXKIuFobJtbBzRrTBPQfE7kO3A5xBKTu44mg5l3t5aCnq6SYNAXiVEBtc0uw8i6XGOp+rh7Ux5mMeYJvRj0tSeHRmMu2nq+T9s+fv35h4yaMXyCn9un7JH+RhlnEwLuJklDOIMnYLEGmGM6o96+WvWvVsb0uufyVUVg7A==" +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/target-list-page-2.golden.json b/src/handlers/gateway/__fixtures__/target-list-page-2.golden.json new file mode 100644 index 000000000..3be0a4f25 --- /dev/null +++ b/src/handlers/gateway/__fixtures__/target-list-page-2.golden.json @@ -0,0 +1,14 @@ +{ + "items": [ + { + "targetId": "ZZHBZX71VQ", + "name": "agentcore-cli-gateway-read-target-a2", + "status": "READY", + "createdAt": "2026-07-29T22:20:25.993Z", + "updatedAt": "2026-07-29T22:20:29.823Z", + "description": "AgentCore CLI persistent Gateway Target read fixture", + "lastSynchronizedAt": "2026-07-29T22:20:29.628Z", + "targetType": "MCP_SERVER" + } + ] +} \ No newline at end of file diff --git a/src/handlers/gateway/gateway.fixture.test.tsx b/src/handlers/gateway/gateway.fixture.test.tsx new file mode 100644 index 000000000..139bcdec8 --- /dev/null +++ b/src/handlers/gateway/gateway.fixture.test.tsx @@ -0,0 +1,180 @@ +import { describe, expect, test } from "bun:test"; +import { join } from "node:path"; +import { CoreClient } from "../../core"; +import { + createSilentLogger, + fixtureFactories, + matchGolden, + TestGlobalConfigAccessor, + testIO, +} from "../../testing"; +import { createRootHandler } from "../index"; + +const REGION = "us-west-2"; +const FIXTURES = join(import.meta.dir, "__fixtures__"); +const GATEWAY_ID = "agentcore-cli-gateway-read-fixture-a-l6opkbe2kd"; +const TARGET_ID = "KALJACI9HO"; +const RULE_GATEWAY_ID = "agentcore-cli-gateway-read-rule-fixture-lhpid2reoy"; +const RULE_ID = "d396c3f4-4591-41b3-a4d5-816e03c32419"; + +// Account 685197708687 owns the persistent read-only fixture graph: +// two listable Gateways, two MCP Targets under GATEWAY_ID, and two Rules under +// RULE_GATEWAY_ID. Record with: +// AWS_PROFILE=e2e-test RECORD=1 bun test src/handlers/gateway/gateway.fixture.test.tsx +function createFixtureCore(): CoreClient { + const { createControlClient, createDataClient, createIamClient } = fixtureFactories(FIXTURES); + return new CoreClient({ + createControlClient, + createDataClient, + createIamClient, + logger: createSilentLogger(), + }); +} + +async function run(args: string[]): Promise { + const io = testIO(); + const root = createRootHandler(createFixtureCore(), { + io: io.io, + logger: createSilentLogger(), + globalConfigAccessor: new TestGlobalConfigAccessor(), + }); + await root.route(["node", "agentcore", ...args, "--region", REGION]); + return io.stdout(); +} + +describe("Gateway fixture-backed reads", () => { + test("gets a Gateway", async () => { + const stdout = await run(["gateway", "get", "--id", GATEWAY_ID]); + matchGolden(FIXTURES, "get.golden.json", stdout); + expect(JSON.parse(stdout).gatewayId).toBe(GATEWAY_ID); + }); + + test("paginates Gateways", async () => { + const page1 = await run(["gateway", "list", "--max-results", "1"]); + matchGolden(FIXTURES, "list-page-1.golden.json", page1); + const first = JSON.parse(page1); + expect(first.items).toHaveLength(1); + expect(first.nextToken).toBeString(); + + const page2 = await run([ + "gateway", + "list", + "--max-results", + "1", + "--next-token", + first.nextToken, + ]); + matchGolden(FIXTURES, "list-page-2.golden.json", page2); + expect(JSON.parse(page2).items).toHaveLength(1); + }); + + test("gets a Gateway Target", async () => { + const stdout = await run([ + "gateway", + "target", + "get", + "--gateway-id", + GATEWAY_ID, + "--target-id", + TARGET_ID, + ]); + matchGolden(FIXTURES, "target-get.golden.json", stdout); + expect(JSON.parse(stdout).targetId).toBe(TARGET_ID); + }); + + test("paginates Gateway Targets", async () => { + const page1 = await run([ + "gateway", + "target", + "list", + "--gateway-id", + GATEWAY_ID, + "--max-results", + "1", + ]); + matchGolden(FIXTURES, "target-list-page-1.golden.json", page1); + const first = JSON.parse(page1); + expect(first.items).toHaveLength(1); + expect(first.nextToken).toBeString(); + + const page2 = await run([ + "gateway", + "target", + "list", + "--gateway-id", + GATEWAY_ID, + "--max-results", + "1", + "--next-token", + first.nextToken, + ]); + matchGolden(FIXTURES, "target-list-page-2.golden.json", page2); + expect(JSON.parse(page2).items).toHaveLength(1); + }); + + test("gets a Gateway Rule", async () => { + const stdout = await run([ + "gateway", + "rule", + "get", + "--gateway-id", + RULE_GATEWAY_ID, + "--rule-id", + RULE_ID, + ]); + matchGolden(FIXTURES, "rule-get.golden.json", stdout); + expect(JSON.parse(stdout).ruleId).toBe(RULE_ID); + }); + + test("paginates Gateway Rules", async () => { + const page1 = await run([ + "gateway", + "rule", + "list", + "--gateway-id", + RULE_GATEWAY_ID, + "--max-results", + "1", + ]); + matchGolden(FIXTURES, "rule-list-page-1.golden.json", page1); + const first = JSON.parse(page1); + expect(first.gatewayRules).toHaveLength(1); + expect(first.nextToken).toBeString(); + + const page2 = await run([ + "gateway", + "rule", + "list", + "--gateway-id", + RULE_GATEWAY_ID, + "--max-results", + "1", + "--next-token", + first.nextToken, + ]); + matchGolden(FIXTURES, "rule-list-page-2.golden.json", page2); + expect(JSON.parse(page2).gatewayRules).toHaveLength(1); + }); + + test.each([ + ["Gateway", ["gateway", "get", "--id", "missing-gateway-0000000000"]], + [ + "Target", + ["gateway", "target", "get", "--gateway-id", GATEWAY_ID, "--target-id", "MISSING000"], + ], + [ + "Rule", + [ + "gateway", + "rule", + "get", + "--gateway-id", + RULE_GATEWAY_ID, + "--rule-id", + "00000000-0000-4000-8000-000000000000", + ], + ], + ] as const)("propagates recorded not-found errors for %s", async (_label, args) => { + await expect(run([...args])).rejects.toMatchObject({ name: "ResourceNotFoundException" }); + }); +}); diff --git a/src/handlers/gateway/gateway.test.tsx b/src/handlers/gateway/gateway.test.tsx new file mode 100644 index 000000000..9dc3ebe29 --- /dev/null +++ b/src/handlers/gateway/gateway.test.tsx @@ -0,0 +1,321 @@ +import { describe, expect, test } from "bun:test"; +import type { + GatewayRuleDetail, + GatewaySummary, + GetGatewayResponse, + GetGatewayRuleResponse, + GetGatewayTargetResponse, + ListGatewayRulesResponse, + ListGatewaysResponse, + ListGatewayTargetsResponse, + TargetSummary, +} from "@aws-sdk/client-bedrock-agentcore-control"; +import { + createSilentLogger, + TestCoreClient, + TestGlobalConfigAccessor, + testIO, +} from "../../testing"; +import { createRootHandler } from "../index"; + +const REGION = "us-west-2"; +const ENDPOINT = "https://agentcore.example.test"; +const GATEWAY_ID = "gateway-1"; +const TARGET_ID = "target-1"; +const RULE_ID = "rule-1"; + +const gatewayResponse = { + gatewayId: GATEWAY_ID, + name: "fixture-gateway", + status: "READY", +} as GetGatewayResponse; +const targetResponse = { + gatewayArn: `arn:aws:bedrock-agentcore:${REGION}:123456789012:gateway/${GATEWAY_ID}`, + targetId: TARGET_ID, + name: "fixture-target", + status: "READY", +} as GetGatewayTargetResponse; +const ruleResponse = { + ruleId: RULE_ID, + gatewayArn: `arn:aws:bedrock-agentcore:${REGION}:123456789012:gateway/${GATEWAY_ID}`, + priority: 1, + status: "ACTIVE", +} as GetGatewayRuleResponse; + +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 command hierarchy", () => { + test("registers the Gateway read command hierarchy", () => { + const root = createRootHandler(new TestCoreClient(), { + io: testIO().io, + logger: createSilentLogger(), + globalConfigAccessor: new TestGlobalConfigAccessor(), + }); + const gateway = root.children().find((child) => child.name() === "gateway"); + const target = gateway?.children().find((child) => child.name() === "target"); + const rule = gateway?.children().find((child) => child.name() === "rule"); + + expect(gateway?.flags().map((flag) => flag.name)).not.toContain("interactive"); + expect(gateway?.children().map((child) => child.name())).toEqual([ + "get", + "list", + "target", + "rule", + ]); + expect(target?.children().map((child) => child.name())).toEqual(["get", "list"]); + expect(rule?.children().map((child) => child.name())).toEqual(["get", "list"]); + }); + + test.each(["gateway", "gateway target", "gateway rule"])( + "prints help for bare `%s` without a Core call", + async (command) => { + const { core, stdout } = await run(command.split(" ")); + + expect(stdout).toContain(`Usage: agentcore ${command}`); + expect(stdout).toContain("Commands:"); + expect(core.gateway.calls).toEqual([]); + }, + ); +}); + +describe("gateway reads", () => { + test("gets a Gateway and renders the response unchanged", async () => { + const core = new TestCoreClient(); + core.gateway.setGetResponse(gatewayResponse); + + const result = await run(["gateway", "get", "--id", GATEWAY_ID], core); + + expect(result.core.gateway.calls).toEqual([ + { + method: "getGateway", + args: [GATEWAY_ID, { region: REGION }], + }, + ]); + expect(JSON.parse(result.stdout)).toEqual(gatewayResponse); + }); + + test("paginates Gateways with the returned token", async () => { + const first: ListGatewaysResponse = { + items: [{ gatewayId: GATEWAY_ID } as GatewaySummary], + nextToken: "gateway-page-2", + }; + const second: ListGatewaysResponse = { + items: [{ gatewayId: "gateway-2" } as GatewaySummary], + }; + const core = new TestCoreClient(); + core.gateway.setListResponse(first).setListResponse(second, first.nextToken); + + const firstResult = await run(["gateway", "list", "--max-results", "1"], core); + const secondResult = await run( + ["gateway", "list", "--max-results", "1", "--next-token", first.nextToken!], + core, + ); + + expect(JSON.parse(firstResult.stdout)).toEqual(first); + expect(JSON.parse(secondResult.stdout)).toEqual(second); + expect(core.gateway.calls).toEqual([ + { + method: "listGateways", + args: [undefined, 1, { region: REGION }], + }, + { + method: "listGateways", + args: [first.nextToken, 1, { region: REGION }], + }, + ]); + }); + + test("gets a Gateway Target with qualified selectors and endpoint options", async () => { + const core = new TestCoreClient(); + core.gateway.setGetTargetResponse(targetResponse); + + const result = await run( + [ + "gateway", + "target", + "get", + "--gateway-id", + GATEWAY_ID, + "--target-id", + TARGET_ID, + "--endpoint-url", + ENDPOINT, + ], + core, + ); + + expect(result.core.gateway.calls).toEqual([ + { + method: "getGatewayTarget", + args: [GATEWAY_ID, TARGET_ID, { region: REGION, endpointUrl: ENDPOINT }], + }, + ]); + expect(JSON.parse(result.stdout)).toEqual(targetResponse); + }); + + test("paginates Gateway Targets with the parent ID and returned token", async () => { + const first: ListGatewayTargetsResponse = { + items: [{ targetId: TARGET_ID } as TargetSummary], + nextToken: "target-page-2", + }; + const second: ListGatewayTargetsResponse = { + items: [{ targetId: "target-2" } as TargetSummary], + }; + const core = new TestCoreClient(); + core.gateway.setListTargetsResponse(first).setListTargetsResponse(second, first.nextToken); + + const firstResult = await run( + ["gateway", "target", "list", "--gateway-id", GATEWAY_ID, "--max-results", "1"], + core, + ); + const secondResult = await run( + [ + "gateway", + "target", + "list", + "--gateway-id", + GATEWAY_ID, + "--max-results", + "1", + "--next-token", + first.nextToken!, + ], + core, + ); + + expect(JSON.parse(firstResult.stdout)).toEqual(first); + expect(JSON.parse(secondResult.stdout)).toEqual(second); + expect(core.gateway.calls).toEqual([ + { + method: "listGatewayTargets", + args: [GATEWAY_ID, undefined, 1, { region: REGION }], + }, + { + method: "listGatewayTargets", + args: [GATEWAY_ID, first.nextToken, 1, { region: REGION }], + }, + ]); + }); + + test("gets a Gateway Rule with qualified selectors", async () => { + const core = new TestCoreClient(); + core.gateway.setGetRuleResponse(ruleResponse); + + const result = await run( + ["gateway", "rule", "get", "--gateway-id", GATEWAY_ID, "--rule-id", RULE_ID], + core, + ); + + expect(result.core.gateway.calls).toEqual([ + { + method: "getGatewayRule", + args: [GATEWAY_ID, RULE_ID, { region: REGION }], + }, + ]); + expect(JSON.parse(result.stdout)).toEqual(ruleResponse); + }); + + test("paginates Gateway Rules with the parent ID and returned token", async () => { + const first: ListGatewayRulesResponse = { + gatewayRules: [{ ruleId: RULE_ID } as GatewayRuleDetail], + nextToken: "rule-page-2", + }; + const second: ListGatewayRulesResponse = { + gatewayRules: [{ ruleId: "rule-2" } as GatewayRuleDetail], + }; + const core = new TestCoreClient(); + core.gateway.setListRulesResponse(first).setListRulesResponse(second, first.nextToken); + + const firstResult = await run( + ["gateway", "rule", "list", "--gateway-id", GATEWAY_ID, "--max-results", "1"], + core, + ); + const secondResult = await run( + [ + "gateway", + "rule", + "list", + "--gateway-id", + GATEWAY_ID, + "--max-results", + "1", + "--next-token", + first.nextToken!, + ], + core, + ); + + expect(JSON.parse(firstResult.stdout)).toEqual(first); + expect(JSON.parse(secondResult.stdout)).toEqual(second); + expect(core.gateway.calls).toEqual([ + { + method: "listGatewayRules", + args: [GATEWAY_ID, undefined, 1, { region: REGION }], + }, + { + method: "listGatewayRules", + args: [GATEWAY_ID, first.nextToken, 1, { region: REGION }], + }, + ]); + }); +}); + +describe("gateway validation and errors", () => { + test.each([ + ["Gateway get", ["gateway", "get"], /--id/], + ["Target get parent", ["gateway", "target", "get"], /--gateway-id/], + ["Target get child", ["gateway", "target", "get", "--gateway-id", GATEWAY_ID], /--target-id/], + ["Target list", ["gateway", "target", "list"], /--gateway-id/], + ["Rule get parent", ["gateway", "rule", "get"], /--gateway-id/], + ["Rule get child", ["gateway", "rule", "get", "--gateway-id", GATEWAY_ID], /--rule-id/], + ["Rule list", ["gateway", "rule", "list"], /--gateway-id/], + ] as const)( + "rejects a missing selector for %s 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 a non-numeric max-results value before calling Core", async () => { + const core = new TestCoreClient(); + + await expect(run(["gateway", "list", "--max-results", "not-a-number"], core)).rejects.toThrow( + /Invalid value for option '--max-results'/, + ); + expect(core.gateway.calls).toEqual([]); + }); + + test.each([ + ["Gateway", ["gateway", "get", "--id", "missing-gateway"]], + [ + "Target", + ["gateway", "target", "get", "--gateway-id", GATEWAY_ID, "--target-id", "missing-target"], + ], + ["Rule", ["gateway", "rule", "get", "--gateway-id", GATEWAY_ID, "--rule-id", "missing-rule"]], + ] as const)("propagates ResourceNotFoundException from %s get", async (_name, args) => { + const error = new Error("resource not found"); + error.name = "ResourceNotFoundException"; + const core = new TestCoreClient(); + core.gateway.setError(error); + + await expect(run([...args], core)).rejects.toMatchObject({ + name: "ResourceNotFoundException", + }); + }); +}); diff --git a/src/handlers/gateway/get/index.tsx b/src/handlers/gateway/get/index.tsx new file mode 100644 index 000000000..c96981a4a --- /dev/null +++ b/src/handlers/gateway/get/index.tsx @@ -0,0 +1,22 @@ +import z from "zod"; +import { InputValidationError } from "../../../errors"; +import { createHandler, flag } from "../../../router"; +import { JsonRendererKey } from "../../../tui"; +import type { Core } from "../../types"; +import { coreOptsFromCtx } from "../../utils"; + +export const createGetGatewayHandler = (core: Core) => + createHandler({ + name: "get", + description: "get an AgentCore Gateway", + flags: [flag("id", "the ID of the Gateway", z.string().optional())], + handle: async (ctx, flags) => { + if (!flags.id) { + throw new InputValidationError("required option '--id ' not specified"); + } + + ctx + .require(JsonRendererKey) + .renderJson(await core.gateway.getGateway(flags.id, coreOptsFromCtx(ctx))); + }, + }); diff --git a/src/handlers/gateway/index.tsx b/src/handlers/gateway/index.tsx new file mode 100644 index 000000000..0c431ca14 --- /dev/null +++ b/src/handlers/gateway/index.tsx @@ -0,0 +1,17 @@ +import type { AppIO } from "../../io"; +import { Router } from "../../router"; +import { createHelpDefault } from "../help"; +import type { Core } from "../types"; +import { createGetGatewayHandler } from "./get"; +import { createListGatewaysHandler } from "./list"; +import { createGatewayRuleHandler } from "./rule"; +import { createGatewayTargetHandler } from "./target"; + +export function createGatewayHandler(core: Core, io: AppIO): Router { + return new Router("gateway", "inspect AgentCore Gateways") + .default(createHelpDefault(io)) + .handler(createGetGatewayHandler(core)) + .handler(createListGatewaysHandler(core)) + .handler(createGatewayTargetHandler(core, io)) + .handler(createGatewayRuleHandler(core, io)); +} diff --git a/src/handlers/gateway/list/index.tsx b/src/handlers/gateway/list/index.tsx new file mode 100644 index 000000000..a4b4074ce --- /dev/null +++ b/src/handlers/gateway/list/index.tsx @@ -0,0 +1,26 @@ +import z from "zod"; +import { createHandler, flag } from "../../../router"; +import { JsonRendererKey } from "../../../tui"; +import type { Core } from "../../types"; +import { coreOptsFromCtx } from "../../utils"; + +export const createListGatewaysHandler = (core: Core) => + createHandler({ + name: "list", + description: "list AgentCore Gateways", + flags: [ + flag("next-token", "pagination token returned by a previous request", z.string().optional()), + flag("max-results", "maximum number of items to return", z.number().optional()), + ], + handle: async (ctx, flags) => { + ctx + .require(JsonRendererKey) + .renderJson( + await core.gateway.listGateways( + flags["next-token"], + flags["max-results"], + coreOptsFromCtx(ctx), + ), + ); + }, + }); diff --git a/src/handlers/gateway/rule/get/index.tsx b/src/handlers/gateway/rule/get/index.tsx new file mode 100644 index 000000000..6c3a43672 --- /dev/null +++ b/src/handlers/gateway/rule/get/index.tsx @@ -0,0 +1,34 @@ +import z from "zod"; +import { InputValidationError } from "../../../../errors"; +import { createHandler, flag } from "../../../../router"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx } from "../../../utils"; + +export const createGetGatewayRuleHandler = (core: Core) => + createHandler({ + name: "get", + description: "get an AgentCore Gateway Rule", + flags: [ + flag("gateway-id", "the ID of the Gateway", z.string().optional()), + flag("rule-id", "the ID of the Gateway Rule", z.string().optional()), + ], + handle: async (ctx, flags) => { + if (!flags["gateway-id"]) { + throw new InputValidationError("required option '--gateway-id ' not specified"); + } + if (!flags["rule-id"]) { + throw new InputValidationError("required option '--rule-id ' not specified"); + } + + ctx + .require(JsonRendererKey) + .renderJson( + await core.gateway.getGatewayRule( + flags["gateway-id"], + flags["rule-id"], + coreOptsFromCtx(ctx), + ), + ); + }, + }); diff --git a/src/handlers/gateway/rule/index.tsx b/src/handlers/gateway/rule/index.tsx new file mode 100644 index 000000000..40ed75f68 --- /dev/null +++ b/src/handlers/gateway/rule/index.tsx @@ -0,0 +1,13 @@ +import type { AppIO } from "../../../io"; +import { Router } from "../../../router"; +import { createHelpDefault } from "../../help"; +import type { Core } from "../../types"; +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(createGetGatewayRuleHandler(core)) + .handler(createListGatewayRulesHandler(core)); +} diff --git a/src/handlers/gateway/rule/list/index.tsx b/src/handlers/gateway/rule/list/index.tsx new file mode 100644 index 000000000..981612c8b --- /dev/null +++ b/src/handlers/gateway/rule/list/index.tsx @@ -0,0 +1,33 @@ +import z from "zod"; +import { InputValidationError } from "../../../../errors"; +import { createHandler, flag } from "../../../../router"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx } from "../../../utils"; + +export const createListGatewayRulesHandler = (core: Core) => + createHandler({ + name: "list", + description: "list rules for an AgentCore Gateway", + flags: [ + flag("gateway-id", "the ID of the Gateway", z.string().optional()), + flag("next-token", "pagination token returned by a previous request", z.string().optional()), + flag("max-results", "maximum number of items to return", z.number().optional()), + ], + handle: async (ctx, flags) => { + if (!flags["gateway-id"]) { + throw new InputValidationError("required option '--gateway-id ' not specified"); + } + + ctx + .require(JsonRendererKey) + .renderJson( + await core.gateway.listGatewayRules( + flags["gateway-id"], + flags["next-token"], + flags["max-results"], + coreOptsFromCtx(ctx), + ), + ); + }, + }); diff --git a/src/handlers/gateway/target/get/index.tsx b/src/handlers/gateway/target/get/index.tsx new file mode 100644 index 000000000..fda1c1b70 --- /dev/null +++ b/src/handlers/gateway/target/get/index.tsx @@ -0,0 +1,34 @@ +import z from "zod"; +import { InputValidationError } from "../../../../errors"; +import { createHandler, flag } from "../../../../router"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx } from "../../../utils"; + +export const createGetGatewayTargetHandler = (core: Core) => + createHandler({ + name: "get", + description: "get an AgentCore Gateway Target", + flags: [ + flag("gateway-id", "the ID of the Gateway", z.string().optional()), + flag("target-id", "the ID of the Gateway Target", z.string().optional()), + ], + handle: async (ctx, flags) => { + if (!flags["gateway-id"]) { + throw new InputValidationError("required option '--gateway-id ' not specified"); + } + if (!flags["target-id"]) { + throw new InputValidationError("required option '--target-id ' not specified"); + } + + ctx + .require(JsonRendererKey) + .renderJson( + await core.gateway.getGatewayTarget( + flags["gateway-id"], + flags["target-id"], + coreOptsFromCtx(ctx), + ), + ); + }, + }); diff --git a/src/handlers/gateway/target/index.tsx b/src/handlers/gateway/target/index.tsx new file mode 100644 index 000000000..96f05dee9 --- /dev/null +++ b/src/handlers/gateway/target/index.tsx @@ -0,0 +1,13 @@ +import type { AppIO } from "../../../io"; +import { Router } from "../../../router"; +import { createHelpDefault } from "../../help"; +import type { Core } from "../../types"; +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(createGetGatewayTargetHandler(core)) + .handler(createListGatewayTargetsHandler(core)); +} diff --git a/src/handlers/gateway/target/list/index.tsx b/src/handlers/gateway/target/list/index.tsx new file mode 100644 index 000000000..e5e8e187e --- /dev/null +++ b/src/handlers/gateway/target/list/index.tsx @@ -0,0 +1,33 @@ +import z from "zod"; +import { InputValidationError } from "../../../../errors"; +import { createHandler, flag } from "../../../../router"; +import { JsonRendererKey } from "../../../../tui"; +import type { Core } from "../../../types"; +import { coreOptsFromCtx } from "../../../utils"; + +export const createListGatewayTargetsHandler = (core: Core) => + createHandler({ + name: "list", + description: "list targets for an AgentCore Gateway", + flags: [ + flag("gateway-id", "the ID of the Gateway", z.string().optional()), + flag("next-token", "pagination token returned by a previous request", z.string().optional()), + flag("max-results", "maximum number of items to return", z.number().optional()), + ], + handle: async (ctx, flags) => { + if (!flags["gateway-id"]) { + throw new InputValidationError("required option '--gateway-id ' not specified"); + } + + ctx + .require(JsonRendererKey) + .renderJson( + await core.gateway.listGatewayTargets( + flags["gateway-id"], + flags["next-token"], + flags["max-results"], + coreOptsFromCtx(ctx), + ), + ); + }, + }); diff --git a/src/handlers/gateway/types.tsx b/src/handlers/gateway/types.tsx new file mode 100644 index 000000000..0ed370130 --- /dev/null +++ b/src/handlers/gateway/types.tsx @@ -0,0 +1,40 @@ +import type { + GetGatewayResponse, + GetGatewayRuleResponse, + GetGatewayTargetResponse, + ListGatewayRulesResponse, + ListGatewaysResponse, + ListGatewayTargetsResponse, +} from "@aws-sdk/client-bedrock-agentcore-control"; +import type { CoreOptions } from "../../core/types"; + +export interface CoreGatewayClient { + getGateway(id: string, options: CoreOptions): Promise; + listGateways( + nextToken: string | undefined, + maxResults: number | undefined, + options: CoreOptions, + ): Promise; + getGatewayTarget( + gatewayId: string, + targetId: string, + options: CoreOptions, + ): Promise; + listGatewayTargets( + gatewayId: string, + nextToken: string | undefined, + maxResults: number | undefined, + options: CoreOptions, + ): Promise; + getGatewayRule( + gatewayId: string, + ruleId: string, + options: CoreOptions, + ): Promise; + listGatewayRules( + gatewayId: string, + nextToken: string | undefined, + maxResults: number | undefined, + options: CoreOptions, + ): Promise; +} diff --git a/src/handlers/index.tsx b/src/handlers/index.tsx index 4946607b3..39897fb6b 100644 --- a/src/handlers/index.tsx +++ b/src/handlers/index.tsx @@ -1,5 +1,6 @@ import { Router } from "../router"; import { createEvalHandler } from "./eval/index.tsx"; +import { createGatewayHandler } from "./gateway/index.tsx"; import { createHarnessHandler } from "./harness/index.tsx"; import { createIdentityHandler } from "./identity/index.tsx"; import { createMemoryHandler } from "./memory/index.tsx"; @@ -46,6 +47,7 @@ export function createRootHandler(core: Core, config: RootHandlerConfig): Router root.handler(createIdentityHandler(core, io)); root.handler(createRuntimeHandler(core, io)); root.handler(createMemoryHandler(core, io)); + root.handler(createGatewayHandler(core, io)); root.handler(createEvalHandler(core, io)); root.handler(createConfigHandler()); root.handler(createProjectHandler({ projectManager: core.projectManager })); diff --git a/src/handlers/root.test.tsx b/src/handlers/root.test.tsx index 34b517d00..b3f4e3386 100644 --- a/src/handlers/root.test.tsx +++ b/src/handlers/root.test.tsx @@ -15,6 +15,7 @@ describe("createRootHandler", () => { "identity", "runtime", "memory", + "gateway", "eval", "config", "project", diff --git a/src/handlers/types.tsx b/src/handlers/types.tsx index d41f66cc0..f129805a8 100644 --- a/src/handlers/types.tsx +++ b/src/handlers/types.tsx @@ -1,4 +1,5 @@ import type { CoreEvalClient } from "./eval/types.tsx"; +import type { CoreGatewayClient } from "./gateway/types.tsx"; import type { CoreHarnessClient } from "./harness/types.tsx"; import type { CoreIdentityClient } from "./identity/types.tsx"; import type { CoreMemoryClient } from "./memory/types.tsx"; @@ -11,6 +12,7 @@ export interface Core { identity: CoreIdentityClient; memory: CoreMemoryClient; runtime: CoreRuntimeClient; + gateway: CoreGatewayClient; eval: CoreEvalClient; projectManager: ProjectManager; } diff --git a/src/testing/TestCoreClient.tsx b/src/testing/TestCoreClient.tsx index 54ef05cdb..b80546f97 100644 --- a/src/testing/TestCoreClient.tsx +++ b/src/testing/TestCoreClient.tsx @@ -8,6 +8,9 @@ import type { DeleteHarnessEndpointResponse, DeleteHarnessRequest, DeleteHarnessResponse, + GetGatewayResponse, + GetGatewayRuleResponse, + GetGatewayTargetResponse, GetApiKeyCredentialProviderResponse, GetHarnessResponse, GetHarnessEndpointResponse, @@ -22,6 +25,9 @@ import type { ListHarnessesResponse, ListHarnessEndpointsResponse, ListHarnessVersionsResponse, + ListGatewayRulesResponse, + ListGatewaysResponse, + ListGatewayTargetsResponse, CreateEvaluatorRequest, CreateEvaluatorResponse, DeleteEvaluatorResponse, @@ -45,6 +51,7 @@ 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 { CoreIdentityClient, CreateApiKeyCredentialProviderInput, @@ -109,6 +116,12 @@ const DEFAULT_UPDATE_API_KEY_RESPONSE = {} as UpdateApiKeyCredentialProviderResp const DEFAULT_DELETE_API_KEY_RESPONSE = {} as DeleteApiKeyCredentialProviderResponse; const DEFAULT_GET_MEMORY_RESPONSE = {} as GetMemoryOutput; const DEFAULT_LIST_MEMORIES_RESPONSE: ListMemoriesOutput = { memories: [] }; +const DEFAULT_GET_GATEWAY_RESPONSE = {} as GetGatewayResponse; +const DEFAULT_LIST_GATEWAYS_RESPONSE: ListGatewaysResponse = { items: [] }; +const DEFAULT_GET_GATEWAY_TARGET_RESPONSE = {} as GetGatewayTargetResponse; +const DEFAULT_LIST_GATEWAY_TARGETS_RESPONSE: ListGatewayTargetsResponse = { items: [] }; +const DEFAULT_GET_GATEWAY_RULE_RESPONSE = {} as GetGatewayRuleResponse; +const DEFAULT_LIST_GATEWAY_RULES_RESPONSE: ListGatewayRulesResponse = { gatewayRules: [] }; const DEFAULT_GET_RUNTIME_RESPONSE = {} as GetAgentRuntimeResponse; const DEFAULT_GET_RUNTIME_ENDPOINT_RESPONSE = {} as GetAgentRuntimeEndpointResponse; const DEFAULT_LIST_RUNTIMES_RESPONSE: ListAgentRuntimesResponse = { agentRuntimes: [] }; @@ -637,6 +650,129 @@ export class TestMemoryClient implements CoreMemoryClient { } } +export class TestGatewayClient implements CoreGatewayClient { + readonly calls: RecordedCall[] = []; + + private getResponse: GetGatewayResponse = DEFAULT_GET_GATEWAY_RESPONSE; + private listResponses = new Map(); + private getTargetResponse: GetGatewayTargetResponse = DEFAULT_GET_GATEWAY_TARGET_RESPONSE; + private listTargetResponses = new Map(); + private getRuleResponse: GetGatewayRuleResponse = DEFAULT_GET_GATEWAY_RULE_RESPONSE; + private listRuleResponses = new Map(); + private error?: Error; + + setGetResponse(response: GetGatewayResponse): this { + this.getResponse = response; + return this; + } + + setListResponse(response: ListGatewaysResponse, forNextToken?: string): this { + this.listResponses.set(forNextToken, response); + return this; + } + + setGetTargetResponse(response: GetGatewayTargetResponse): this { + this.getTargetResponse = response; + return this; + } + + setListTargetsResponse(response: ListGatewayTargetsResponse, forNextToken?: string): this { + this.listTargetResponses.set(forNextToken, response); + return this; + } + + setGetRuleResponse(response: GetGatewayRuleResponse): this { + this.getRuleResponse = response; + return this; + } + + setListRulesResponse(response: ListGatewayRulesResponse, forNextToken?: string): this { + this.listRuleResponses.set(forNextToken, response); + return this; + } + + setError(error: Error | undefined): this { + this.error = error; + return this; + } + + async getGateway(id: string, options: CoreOptions): Promise { + this.calls.push({ method: "getGateway", args: [id, options] }); + if (this.error) throw this.error; + return this.getResponse; + } + + async listGateways( + nextToken: string | undefined, + maxResults: number | undefined, + options: CoreOptions, + ): Promise { + this.calls.push({ method: "listGateways", args: [nextToken, maxResults, options] }); + if (this.error) throw this.error; + return ( + this.listResponses.get(nextToken) ?? + this.listResponses.get(undefined) ?? + DEFAULT_LIST_GATEWAYS_RESPONSE + ); + } + + async getGatewayTarget( + gatewayId: string, + targetId: string, + options: CoreOptions, + ): Promise { + this.calls.push({ method: "getGatewayTarget", args: [gatewayId, targetId, options] }); + if (this.error) throw this.error; + return this.getTargetResponse; + } + + async listGatewayTargets( + gatewayId: string, + nextToken: string | undefined, + maxResults: number | undefined, + options: CoreOptions, + ): Promise { + this.calls.push({ + method: "listGatewayTargets", + args: [gatewayId, nextToken, maxResults, options], + }); + if (this.error) throw this.error; + return ( + this.listTargetResponses.get(nextToken) ?? + this.listTargetResponses.get(undefined) ?? + DEFAULT_LIST_GATEWAY_TARGETS_RESPONSE + ); + } + + async getGatewayRule( + gatewayId: string, + ruleId: string, + options: CoreOptions, + ): Promise { + this.calls.push({ method: "getGatewayRule", args: [gatewayId, ruleId, options] }); + if (this.error) throw this.error; + return this.getRuleResponse; + } + + async listGatewayRules( + gatewayId: string, + nextToken: string | undefined, + maxResults: number | undefined, + options: CoreOptions, + ): Promise { + this.calls.push({ + method: "listGatewayRules", + args: [gatewayId, nextToken, maxResults, options], + }); + if (this.error) throw this.error; + return ( + this.listRuleResponses.get(nextToken) ?? + this.listRuleResponses.get(undefined) ?? + DEFAULT_LIST_GATEWAY_RULES_RESPONSE + ); + } +} + type TestCoreClientOptions = { logger?: Logger; }; @@ -795,6 +931,7 @@ export class TestCoreClient implements Core { readonly identity = new TestIdentityClient(); readonly memory = new TestMemoryClient(); readonly runtime = new TestRuntimeClient(); + readonly gateway = new TestGatewayClient(); readonly eval = new TestEvalClient(); readonly projectManager: ProjectManager; diff --git a/src/testing/index.tsx b/src/testing/index.tsx index 7ea3ce891..618339716 100644 --- a/src/testing/index.tsx +++ b/src/testing/index.tsx @@ -4,6 +4,7 @@ export { testIO, type TestIO } from "./testIO"; export { tick, waitFor } from "./timing"; export { TestCoreClient, + TestGatewayClient, TestHarnessClient, TestMemoryClient, TestRuntimeClient,