diff --git a/src/handlers/gateway/__fixtures__/GetGatewayTargetCommand.5c220cde576e6df6.json b/src/handlers/gateway/__fixtures__/GetGatewayTargetCommand.5c220cde576e6df6.json new file mode 100644 index 000000000..2e860a0c3 --- /dev/null +++ b/src/handlers/gateway/__fixtures__/GetGatewayTargetCommand.5c220cde576e6df6.json @@ -0,0 +1,38 @@ +{ + "gatewayArn": "arn:aws:bedrock-agentcore:us-east-1:685197708687:gateway/agentcore-cli-gateway-read-connector-gkzcxxkc5e", + "targetId": "Z3FQ0H8JCK", + "createdAt": { + "$date": "2026-08-05T20:20:45.351Z" + }, + "updatedAt": { + "$date": "2026-08-05T20:20:51.210Z" + }, + "status": "READY", + "name": "agentcore-cli-gateway-read-connector-openai", + "targetConfiguration": { + "inference": { + "connector": { + "source": { + "connectorId": "openai" + } + } + } + }, + "credentialProviderConfigurations": [ + { + "credentialProviderType": "API_KEY", + "credentialProvider": { + "apiKeyCredentialProvider": { + "providerArn": "arn:aws:bedrock-agentcore:us-east-1:685197708687:token-vault/default/apikeycredentialprovider/DONOTDELETEe2eOAI", + "credentialParameterName": "Authorization", + "credentialPrefix": "Bearer", + "credentialLocation": "HEADER" + } + } + } + ], + "description": "AgentCore CLI persistent READY Gateway Connector Target fixture", + "lastSynchronizedAt": { + "$date": "2026-08-05T20:20:51.000Z" + } +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/ListGatewayTargetsCommand.34041f83759ffb99.json b/src/handlers/gateway/__fixtures__/ListGatewayTargetsCommand.34041f83759ffb99.json new file mode 100644 index 000000000..cfc4c7480 --- /dev/null +++ b/src/handlers/gateway/__fixtures__/ListGatewayTargetsCommand.34041f83759ffb99.json @@ -0,0 +1,20 @@ +{ + "items": [ + { + "targetId": "Z3FQ0H8JCK", + "name": "agentcore-cli-gateway-read-connector-openai", + "status": "READY", + "createdAt": { + "$date": "2026-08-05T20:20:45.351Z" + }, + "updatedAt": { + "$date": "2026-08-05T20:20:51.210Z" + }, + "description": "AgentCore CLI persistent READY Gateway Connector Target fixture", + "lastSynchronizedAt": { + "$date": "2026-08-05T20:20:51.000Z" + }, + "targetType": "CONNECTOR" + } + ] +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/connector-get.golden.json b/src/handlers/gateway/__fixtures__/connector-get.golden.json new file mode 100644 index 000000000..efafebd65 --- /dev/null +++ b/src/handlers/gateway/__fixtures__/connector-get.golden.json @@ -0,0 +1,32 @@ +{ + "gatewayArn": "arn:aws:bedrock-agentcore:us-east-1:685197708687:gateway/agentcore-cli-gateway-read-connector-gkzcxxkc5e", + "targetId": "Z3FQ0H8JCK", + "createdAt": "2026-08-05T20:20:45.351Z", + "updatedAt": "2026-08-05T20:20:51.210Z", + "status": "READY", + "name": "agentcore-cli-gateway-read-connector-openai", + "targetConfiguration": { + "inference": { + "connector": { + "source": { + "connectorId": "openai" + } + } + } + }, + "credentialProviderConfigurations": [ + { + "credentialProviderType": "API_KEY", + "credentialProvider": { + "apiKeyCredentialProvider": { + "providerArn": "arn:aws:bedrock-agentcore:us-east-1:685197708687:token-vault/default/apikeycredentialprovider/DONOTDELETEe2eOAI", + "credentialParameterName": "Authorization", + "credentialPrefix": "Bearer", + "credentialLocation": "HEADER" + } + } + } + ], + "description": "AgentCore CLI persistent READY Gateway Connector Target fixture", + "lastSynchronizedAt": "2026-08-05T20:20:51.000Z" +} \ No newline at end of file diff --git a/src/handlers/gateway/__fixtures__/connector-list.golden.json b/src/handlers/gateway/__fixtures__/connector-list.golden.json new file mode 100644 index 000000000..e984ee9f8 --- /dev/null +++ b/src/handlers/gateway/__fixtures__/connector-list.golden.json @@ -0,0 +1,14 @@ +{ + "items": [ + { + "targetId": "Z3FQ0H8JCK", + "name": "agentcore-cli-gateway-read-connector-openai", + "status": "READY", + "createdAt": "2026-08-05T20:20:45.351Z", + "updatedAt": "2026-08-05T20:20:51.210Z", + "description": "AgentCore CLI persistent READY Gateway Connector Target fixture", + "lastSynchronizedAt": "2026-08-05T20:20:51.000Z", + "targetType": "CONNECTOR" + } + ] +} \ No newline at end of file diff --git a/src/handlers/gateway/connector/get/index.tsx b/src/handlers/gateway/connector/get/index.tsx new file mode 100644 index 000000000..5932fcddb --- /dev/null +++ b/src/handlers/gateway/connector/get/index.tsx @@ -0,0 +1,42 @@ +import type { 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 } from "../../../utils"; + +function isConnectorTarget(configuration: TargetConfiguration | undefined): boolean { + return ( + configuration?.mcp?.connector !== undefined || configuration?.inference?.connector !== undefined + ); +} + +export const createGetGatewayConnectorHandler = (core: Core) => + createHandler({ + name: "get", + description: "get a connector configured for an AgentCore Gateway", + flags: [ + flag("gateway-id", "the ID of the Gateway", z.string().optional()), + flag("id", "the ID of the connector-backed Gateway Target", z.string().optional()), + ], + handle: async (ctx, flags) => { + if (!flags["gateway-id"]) { + throw new InputValidationError("required option '--gateway-id ' not specified"); + } + if (!flags.id) { + throw new InputValidationError("required option '--id ' not specified"); + } + + const target = await core.gateway.getGatewayTarget( + flags["gateway-id"], + flags.id, + coreOptsFromCtx(ctx), + ); + if (!isConnectorTarget(target.targetConfiguration)) { + throw new InputValidationError(`Gateway Target "${flags.id}" is not connector-backed`); + } + + ctx.require(JsonRendererKey).renderJson(target); + }, + }); diff --git a/src/handlers/gateway/connector/index.tsx b/src/handlers/gateway/connector/index.tsx new file mode 100644 index 000000000..c4eddc556 --- /dev/null +++ b/src/handlers/gateway/connector/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 { createGetGatewayConnectorHandler } from "./get"; +import { createListGatewayConnectorsHandler } from "./list"; + +export function createGatewayConnectorHandler(core: Core, io: AppIO): Router { + return new Router("connector", "inspect connectors configured for an AgentCore Gateway") + .default(createHelpDefault(io)) + .handler(createGetGatewayConnectorHandler(core)) + .handler(createListGatewayConnectorsHandler(core)); +} diff --git a/src/handlers/gateway/connector/list/index.tsx b/src/handlers/gateway/connector/list/index.tsx new file mode 100644 index 000000000..6448b6122 --- /dev/null +++ b/src/handlers/gateway/connector/list/index.tsx @@ -0,0 +1,35 @@ +import { TargetType } 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 } from "../../../utils"; + +export const createListGatewayConnectorsHandler = (core: Core) => + createHandler({ + name: "list", + description: "list connectors configured 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"); + } + + const response = await core.gateway.listGatewayTargets( + flags["gateway-id"], + flags["next-token"], + flags["max-results"], + coreOptsFromCtx(ctx), + ); + + ctx.require(JsonRendererKey).renderJson({ + ...response, + items: response.items?.filter((target) => target.targetType === TargetType.CONNECTOR), + }); + }, + }); diff --git a/src/handlers/gateway/gateway.fixture.test.tsx b/src/handlers/gateway/gateway.fixture.test.tsx index 139bcdec8..e45bdf14a 100644 --- a/src/handlers/gateway/gateway.fixture.test.tsx +++ b/src/handlers/gateway/gateway.fixture.test.tsx @@ -11,15 +11,18 @@ import { import { createRootHandler } from "../index"; const REGION = "us-west-2"; +const CONNECTOR_REGION = "us-east-1"; 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"; +const CONNECTOR_GATEWAY_ID = "agentcore-cli-gateway-read-connector-gkzcxxkc5e"; +const CONNECTOR_ID = "Z3FQ0H8JCK"; -// 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: +// The e2e-test profile owns the persistent read-only fixture graph: +// listable Gateways and Targets in REGION, plus one READY connector Target under +// CONNECTOR_GATEWAY_ID in CONNECTOR_REGION. 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); @@ -31,14 +34,14 @@ function createFixtureCore(): CoreClient { }); } -async function run(args: string[]): Promise { +async function run(args: string[], region = REGION): Promise { const io = testIO(); const root = createRootHandler(createFixtureCore(), { io: io.io, logger: createSilentLogger(), globalConfigAccessor: new TestGlobalConfigAccessor(), }); - await root.route(["node", "agentcore", ...args, "--region", REGION]); + await root.route(["node", "agentcore", ...args, "--region", region]); return io.stdout(); } @@ -112,6 +115,26 @@ describe("Gateway fixture-backed reads", () => { expect(JSON.parse(page2).items).toHaveLength(1); }); + test("gets a Gateway Connector", async () => { + const stdout = await run( + ["gateway", "connector", "get", "--gateway-id", CONNECTOR_GATEWAY_ID, "--id", CONNECTOR_ID], + CONNECTOR_REGION, + ); + matchGolden(FIXTURES, "connector-get.golden.json", stdout); + expect(JSON.parse(stdout).targetId).toBe(CONNECTOR_ID); + }); + + test("lists Gateway Connectors", async () => { + const stdout = await run( + ["gateway", "connector", "list", "--gateway-id", CONNECTOR_GATEWAY_ID], + CONNECTOR_REGION, + ); + matchGolden(FIXTURES, "connector-list.golden.json", stdout); + expect(JSON.parse(stdout).items.map(({ targetId }: { targetId: string }) => targetId)).toEqual([ + CONNECTOR_ID, + ]); + }); + test("gets a Gateway Rule", async () => { const stdout = await run([ "gateway", diff --git a/src/handlers/gateway/gateway.test.tsx b/src/handlers/gateway/gateway.test.tsx index 9dc3ebe29..1dc8fe0ae 100644 --- a/src/handlers/gateway/gateway.test.tsx +++ b/src/handlers/gateway/gateway.test.tsx @@ -1,14 +1,15 @@ import { describe, expect, test } from "bun:test"; -import type { - GatewayRuleDetail, - GatewaySummary, - GetGatewayResponse, - GetGatewayRuleResponse, - GetGatewayTargetResponse, - ListGatewayRulesResponse, - ListGatewaysResponse, - ListGatewayTargetsResponse, - TargetSummary, +import { + TargetType, + type GatewayRuleDetail, + type GatewaySummary, + type GetGatewayResponse, + type GetGatewayRuleResponse, + type GetGatewayTargetResponse, + type ListGatewayRulesResponse, + type ListGatewaysResponse, + type ListGatewayTargetsResponse, + type TargetSummary, } from "@aws-sdk/client-bedrock-agentcore-control"; import { createSilentLogger, @@ -66,6 +67,7 @@ describe("gateway command hierarchy", () => { }); const gateway = root.children().find((child) => child.name() === "gateway"); const target = gateway?.children().find((child) => child.name() === "target"); + const connector = gateway?.children().find((child) => child.name() === "connector"); const rule = gateway?.children().find((child) => child.name() === "rule"); expect(gateway?.flags().map((flag) => flag.name)).not.toContain("interactive"); @@ -73,13 +75,15 @@ describe("gateway command hierarchy", () => { "get", "list", "target", + "connector", "rule", ]); expect(target?.children().map((child) => child.name())).toEqual(["get", "list"]); + expect(connector?.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"])( + test.each(["gateway", "gateway target", "gateway connector", "gateway rule"])( "prints help for bare `%s` without a Core call", async (command) => { const { core, stdout } = await run(command.split(" ")); @@ -210,6 +214,67 @@ describe("gateway reads", () => { ]); }); + test.each([ + ["MCP", { mcp: { connector: { source: { connectorId: "web-search" } } } }], + ["inference", { inference: { connector: { source: { connectorId: "openai" } } } }], + ] as const)("gets a configured %s Connector", async (_kind, targetConfiguration) => { + const core = new TestCoreClient(); + core.gateway.setGetTargetResponse({ ...targetResponse, targetConfiguration }); + + const result = await run( + ["gateway", "connector", "get", "--gateway-id", GATEWAY_ID, "--id", TARGET_ID], + core, + ); + + expect(result.core.gateway.calls).toEqual([ + { + method: "getGatewayTarget", + args: [GATEWAY_ID, TARGET_ID, { region: REGION }], + }, + ]); + expect(JSON.parse(result.stdout)).toEqual({ ...targetResponse, targetConfiguration }); + }); + + test("lists only configured Connectors and preserves the service token", async () => { + const response: ListGatewayTargetsResponse = { + items: [ + { targetId: TARGET_ID, targetType: TargetType.CONNECTOR } as TargetSummary, + { targetId: "target-2", targetType: TargetType.MCP_SERVER } as TargetSummary, + ], + nextToken: "target-page-2", + }; + const core = new TestCoreClient(); + core.gateway.setListTargetsResponse(response); + + const result = await run( + ["gateway", "connector", "list", "--gateway-id", GATEWAY_ID, "--max-results", "2"], + core, + ); + + expect(JSON.parse(result.stdout)).toEqual({ + items: [response.items![0]], + nextToken: response.nextToken, + }); + expect(result.core.gateway.calls).toEqual([ + { + method: "listGatewayTargets", + args: [GATEWAY_ID, undefined, 2, { region: REGION }], + }, + ]); + }); + + test("rejects a non-Connector Target from connector get", async () => { + const core = new TestCoreClient(); + core.gateway.setGetTargetResponse({ + ...targetResponse, + targetConfiguration: { mcp: { mcpServer: { endpoint: "https://example.test/mcp" } } }, + }); + + await expect( + run(["gateway", "connector", "get", "--gateway-id", GATEWAY_ID, "--id", TARGET_ID], core), + ).rejects.toThrow(`Gateway Target "${TARGET_ID}" is not connector-backed`); + }); + test("gets a Gateway Rule with qualified selectors", async () => { const core = new TestCoreClient(); core.gateway.setGetRuleResponse(ruleResponse); @@ -279,6 +344,9 @@ describe("gateway validation and errors", () => { ["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/], + ["Connector get parent", ["gateway", "connector", "get"], /--gateway-id/], + ["Connector get child", ["gateway", "connector", "get", "--gateway-id", GATEWAY_ID], /--id/], + ["Connector list", ["gateway", "connector", "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/], diff --git a/src/handlers/gateway/index.tsx b/src/handlers/gateway/index.tsx index 0c431ca14..698e3719e 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 { createGatewayConnectorHandler } from "./connector"; import { createGetGatewayHandler } from "./get"; import { createListGatewaysHandler } from "./list"; import { createGatewayRuleHandler } from "./rule"; @@ -13,5 +14,6 @@ export function createGatewayHandler(core: Core, io: AppIO): Router { .handler(createGetGatewayHandler(core)) .handler(createListGatewaysHandler(core)) .handler(createGatewayTargetHandler(core, io)) + .handler(createGatewayConnectorHandler(core, io)) .handler(createGatewayRuleHandler(core, io)); }