From b41d58f341747723e22c4565956d162a61705ab8 Mon Sep 17 00:00:00 2001 From: sundargthb Date: Thu, 10 Sep 2026 08:03:01 -0700 Subject: [PATCH] feat(gateway-target): add --include-domains for the web-search connector The web search connector's admin-level domainFilter accepts include and exclude, and both AgentCore SDKs expose both. The CLI could only set exclude, so a target restricted to an allowed set of domains was not expressible. Adds --include-domains alongside --exclude-domains on add gateway-target, an Include Domains step in the interactive wizard, and both lists in one domainFilter in the translator. An empty list is omitted rather than sent, since an empty include list would tell the connector to return nothing. The translator had no test file, so this adds one covering include only, exclude only, both together, and the empty cases. --- src/cli/commands/add/types.ts | 5 ++ src/cli/commands/add/validate.ts | 16 ++++--- .../connectors/__tests__/translators.test.ts | 48 +++++++++++++++++++ src/cli/operations/connectors/translators.ts | 14 +++++- src/cli/primitives/GatewayTargetPrimitive.ts | 31 ++++++++---- .../__tests__/GatewayTargetPrimitive.test.ts | 32 +++++++++++++ .../screens/mcp/AddGatewayTargetScreen.tsx | 23 ++++++++- src/cli/tui/screens/mcp/types.ts | 6 +++ .../screens/mcp/useAddGatewayTargetWizard.ts | 20 +++++++- 9 files changed, 177 insertions(+), 18 deletions(-) create mode 100644 src/cli/operations/connectors/__tests__/translators.test.ts diff --git a/src/cli/commands/add/types.ts b/src/cli/commands/add/types.ts index e70cd63b2..b619f0f44 100644 --- a/src/cli/commands/add/types.ts +++ b/src/cli/commands/add/types.ts @@ -108,6 +108,11 @@ export interface AddGatewayTargetOptions { stickinessTimeout?: string; signingService?: string; signingRegion?: string; + /** + * Comma-separated list of domains to restrict web search results to. + * Only applies to --type web-search. + */ + includeDomains?: string; /** * Comma-separated list of domains to exclude from web search results. * Only applies to --type web-search. diff --git a/src/cli/commands/add/validate.ts b/src/cli/commands/add/validate.ts index 3b027fdb2..83b609a29 100644 --- a/src/cli/commands/add/validate.ts +++ b/src/cli/commands/add/validate.ts @@ -437,12 +437,16 @@ export async function validateAddGatewayTargetOptions(options: AddGatewayTargetO } options.type = mappedType; - // --exclude-domains only applies to the web-search connector. - if (options.excludeDomains && !(mappedType === 'connector' && options.connector === 'web-search')) { - return { - valid: false, - error: '--exclude-domains only applies to --connector web-search', - }; + // The domain filters only apply to the web-search connector. + const isWebSearchConnector = mappedType === 'connector' && options.connector === 'web-search'; + for (const flag of ['includeDomains', 'excludeDomains'] as const) { + if (options[flag] && !isWebSearchConnector) { + const name = flag === 'includeDomains' ? '--include-domains' : '--exclude-domains'; + return { + valid: false, + error: `${name} only applies to --connector web-search`, + }; + } } // Gateway is required — a gateway target must be attached to a gateway diff --git a/src/cli/operations/connectors/__tests__/translators.test.ts b/src/cli/operations/connectors/__tests__/translators.test.ts new file mode 100644 index 000000000..e2f0779e5 --- /dev/null +++ b/src/cli/operations/connectors/__tests__/translators.test.ts @@ -0,0 +1,48 @@ +import { translateConnector } from '../translators'; +import { describe, expect, it } from 'vitest'; + +/** The single WebSearch entry the web-search connector always produces. */ +function webSearch(input: { includeDomains?: string[]; excludeDomains?: string[] }) { + const entries = translateConnector({ connectorId: 'web-search', input }); + expect(entries).toHaveLength(1); + expect(entries[0]?.name).toBe('WebSearch'); + return entries[0]!; +} + +describe('translateConnector — web-search', () => { + it('emits no domainFilter when neither list is given', () => { + expect(webSearch({}).parameterValues).toEqual({}); + }); + + it('emits an exclude-only filter', () => { + expect(webSearch({ excludeDomains: ['internal.example.com'] }).parameterValues).toEqual({ + domainFilter: { exclude: ['internal.example.com'] }, + }); + }); + + it('emits an include-only filter', () => { + expect(webSearch({ includeDomains: ['docs.aws.amazon.com'] }).parameterValues).toEqual({ + domainFilter: { include: ['docs.aws.amazon.com'] }, + }); + }); + + it('puts both lists in one domainFilter, since the connector reads them together', () => { + expect( + webSearch({ includeDomains: ['aws.amazon.com'], excludeDomains: ['internal.example.com'] }).parameterValues + ).toEqual({ + domainFilter: { include: ['aws.amazon.com'], exclude: ['internal.example.com'] }, + }); + }); + + it('drops empty lists rather than sending them', () => { + // An empty include list is not the same request as no include list: sending one + // would tell the connector to return nothing. + expect(webSearch({ includeDomains: [], excludeDomains: [] }).parameterValues).toEqual({}); + }); + + it('keeps one list when the other is empty', () => { + expect(webSearch({ includeDomains: ['aws.amazon.com'], excludeDomains: [] }).parameterValues).toEqual({ + domainFilter: { include: ['aws.amazon.com'] }, + }); + }); +}); diff --git a/src/cli/operations/connectors/translators.ts b/src/cli/operations/connectors/translators.ts index 5e8240542..59d4c23ee 100644 --- a/src/cli/operations/connectors/translators.ts +++ b/src/cli/operations/connectors/translators.ts @@ -20,6 +20,7 @@ export interface ParameterOverride { } export interface WebSearchTranslatorInput { + includeDomains?: string[]; excludeDomains?: string[]; } @@ -33,9 +34,20 @@ export type ConnectorTranslatorInput = function translateWebSearch(input: WebSearchTranslatorInput): ConfigurationEntry[] { const parameterValues: Record = {}; + + // Both lists go in one domainFilter, since that is how the connector reads them. + // An empty list is not the same request as an absent one, so empty is left out. + const domainFilter: { include?: string[]; exclude?: string[] } = {}; + if (input.includeDomains && input.includeDomains.length > 0) { + domainFilter.include = input.includeDomains; + } if (input.excludeDomains && input.excludeDomains.length > 0) { - parameterValues.domainFilter = { exclude: input.excludeDomains }; + domainFilter.exclude = input.excludeDomains; } + if (Object.keys(domainFilter).length > 0) { + parameterValues.domainFilter = domainFilter; + } + return [{ name: 'WebSearch', description: '', parameterValues, parameterOverrides: [] }]; } diff --git a/src/cli/primitives/GatewayTargetPrimitive.ts b/src/cli/primitives/GatewayTargetPrimitive.ts index 280d13ad6..58977a481 100644 --- a/src/cli/primitives/GatewayTargetPrimitive.ts +++ b/src/cli/primitives/GatewayTargetPrimitive.ts @@ -277,16 +277,18 @@ export class GatewayTargetPrimitive extends BasePrimitive { + const onceOnly = (flag: string) => (val: string, prev?: string) => { if (prev !== undefined) { throw new ValidationError( - '--exclude-domains may only be specified once. Pass all domains as a single comma-separated value.' + `${flag} may only be specified once. Pass all domains as a single comma-separated value.` ); } return val; }; + const includeDomainsCoercer = onceOnly('--include-domains'); + const excludeDomainsCoercer = onceOnly('--exclude-domains'); addCmd .command('gateway-target') @@ -305,6 +307,11 @@ export class GatewayTargetPrimitive extends BasePrimitive [...acc, val], [] as string[] ) + .option( + '--include-domains ', + 'Comma-separated domains to restrict results to (for --connector web-search) [non-interactive]', + includeDomainsCoercer + ) .option( '--exclude-domains ', 'Comma-separated domains to exclude from results (for --connector web-search) [non-interactive]', @@ -411,6 +418,7 @@ Target types and their options: connector — Wire a managed AWS connector (bedrock-knowledge-bases, web-search) --connector bedrock-knowledge-bases or web-search --knowledge-base-id Project KB name or 10-char external KB id (for KB connectors) + --include-domains Comma-separated domains to restrict results to (for web-search connector) --exclude-domains Comma-separated domains to exclude (for web-search connector) passthrough — Route to an external HTTPS endpoint @@ -638,24 +646,31 @@ Target types and their options: // Web search connector if (connectorId === 'web-search') { - const excludeDomains = - typeof cliOptions.excludeDomains === 'string' - ? cliOptions.excludeDomains + const splitDomains = (value: unknown): string[] | undefined => + typeof value === 'string' + ? value .split(',') .map((d: string) => d.trim()) .filter((d: string) => d.length > 0) : undefined; + const includeDomains = splitDomains(cliOptions.includeDomains); + const excludeDomains = splitDomains(cliOptions.excludeDomains); const config: WebSearchTargetConfig = { targetType: 'webSearch', name: cliOptions.name!, gateway: cliOptions.gateway!, + ...(includeDomains && includeDomains.length > 0 ? { includeDomains } : {}), ...(excludeDomains && excludeDomains.length > 0 ? { excludeDomains } : {}), }; const result = await this.createWebSearchGatewayTarget(config); if (cliOptions.json) { console.log(JSON.stringify({ success: true, toolName: result.toolName })); } else { - const suffix = config.excludeDomains ? ` (excludeDomains=${config.excludeDomains.join(',')})` : ''; + const filters = [ + ...(config.includeDomains ? [`includeDomains=${config.includeDomains.join(',')}`] : []), + ...(config.excludeDomains ? [`excludeDomains=${config.excludeDomains.join(',')}`] : []), + ]; + const suffix = filters.length > 0 ? ` (${filters.join(', ')})` : ''; console.log(`Added web-search gateway target '${result.toolName}' on '${config.gateway}'${suffix}`); } return { ...telemetryAttrs, gateway_target_type: 'web-search' as const }; @@ -1226,7 +1241,7 @@ Target types and their options: const configurations = translateConnector({ connectorId: 'web-search', - input: { excludeDomains: config.excludeDomains }, + input: { includeDomains: config.includeDomains, excludeDomains: config.excludeDomains }, }); const target: AgentCoreGatewayTarget = { diff --git a/src/cli/primitives/__tests__/GatewayTargetPrimitive.test.ts b/src/cli/primitives/__tests__/GatewayTargetPrimitive.test.ts index 6cf352869..da677d099 100644 --- a/src/cli/primitives/__tests__/GatewayTargetPrimitive.test.ts +++ b/src/cli/primitives/__tests__/GatewayTargetPrimitive.test.ts @@ -357,6 +357,38 @@ describe('GatewayTargetPrimitive — createWebSearchGatewayTarget', () => { ]); }); + it('persists includeDomains in configurations when provided', async () => { + const { primitive, getProject } = makePrimitive(emptyProject()); + await primitive.createWebSearchGatewayTarget({ + targetType: 'webSearch', + name: 'ws', + gateway: 'main-gw', + includeDomains: ['docs.aws.amazon.com', 'aws.amazon.com'], + }); + const target = getProject().agentCoreGateways[0]?.targets[0]; + const wsConfig = (target?.configurations ?? []).find(c => c.name === 'WebSearch'); + expect((wsConfig?.parameterValues as any)?.domainFilter).toEqual({ + include: ['docs.aws.amazon.com', 'aws.amazon.com'], + }); + }); + + it('persists both domain lists in one domainFilter when both are given', async () => { + const { primitive, getProject } = makePrimitive(emptyProject()); + await primitive.createWebSearchGatewayTarget({ + targetType: 'webSearch', + name: 'ws', + gateway: 'main-gw', + includeDomains: ['aws.amazon.com'], + excludeDomains: ['internal.example.com'], + }); + const target = getProject().agentCoreGateways[0]?.targets[0]; + const wsConfig = (target?.configurations ?? []).find(c => c.name === 'WebSearch'); + expect((wsConfig?.parameterValues as any)?.domainFilter).toEqual({ + include: ['aws.amazon.com'], + exclude: ['internal.example.com'], + }); + }); + it('rejects a duplicate target name on the same gateway', async () => { const { primitive } = makePrimitive(emptyProject()); await primitive.createWebSearchGatewayTarget({ diff --git a/src/cli/tui/screens/mcp/AddGatewayTargetScreen.tsx b/src/cli/tui/screens/mcp/AddGatewayTargetScreen.tsx index 7721d0d33..67f160680 100644 --- a/src/cli/tui/screens/mcp/AddGatewayTargetScreen.tsx +++ b/src/cli/tui/screens/mcp/AddGatewayTargetScreen.tsx @@ -108,6 +108,7 @@ export function AddGatewayTargetScreen({ const isPassthroughEndpointStep = wizard.step === 'passthrough-endpoint'; const isPassthroughProtocolStep = wizard.step === 'passthrough-protocol'; const isPassthroughStickinessStep = wizard.step === 'passthrough-stickiness'; + const isIncludeDomainsStep = wizard.step === 'include-domains'; const isExcludeDomainsStep = wizard.step === 'exclude-domains'; const isConfirmStep = wizard.step === 'confirm'; const isAuthStep = isOutboundAuthStep || isApiGatewayAuthStep; @@ -425,6 +426,7 @@ export function AddGatewayTargetScreen({ targetType: 'webSearch', name: c.name, gateway: c.gateway!, + ...(c.includeDomains && c.includeDomains.length > 0 ? { includeDomains: c.includeDomains } : {}), ...(c.excludeDomains && c.excludeDomains.length > 0 ? { excludeDomains: c.excludeDomains } : {}), }); } else { @@ -812,6 +814,22 @@ export function AddGatewayTargetScreen({ /> )} + {isIncludeDomainsStep && ( + { + const domains = value + .split(',') + .map(d => d.trim()) + .filter(d => d.length > 0); + wizard.setIncludeDomains(domains.length > 0 ? domains : undefined); + }} + onCancel={() => wizard.goBack()} + /> + )} + {isExcludeDomainsStep && ( = { 'passthrough-stickiness': 'Stickiness', 'signing-service': 'Signing Service', 'signing-region': 'Signing Region', + 'include-domains': 'Include Domains', 'exclude-domains': 'Exclude Domains', confirm: 'Confirm', }; diff --git a/src/cli/tui/screens/mcp/useAddGatewayTargetWizard.ts b/src/cli/tui/screens/mcp/useAddGatewayTargetWizard.ts index 2d7af3f36..ff6841c10 100644 --- a/src/cli/tui/screens/mcp/useAddGatewayTargetWizard.ts +++ b/src/cli/tui/screens/mcp/useAddGatewayTargetWizard.ts @@ -58,7 +58,7 @@ export function useAddGatewayTargetWizard( break; case 'connector': if (config.connectorId === 'web-search') { - baseSteps.push('gateway', 'exclude-domains'); + baseSteps.push('gateway', 'include-domains', 'exclude-domains'); } else { baseSteps.push('kb-select', 'gateway'); } @@ -129,7 +129,7 @@ export function useAddGatewayTargetWizard( ...c, targetType, connectorId: resolvedConnectorId, - ...(resolvedConnectorId !== 'web-search' ? { excludeDomains: undefined } : {}), + ...(resolvedConnectorId !== 'web-search' ? { includeDomains: undefined, excludeDomains: undefined } : {}), })); switch (targetType) { case 'apiGateway': @@ -349,6 +349,21 @@ export function useAddGatewayTargetWizard( [goToNextStep] ); + /** + * Set the optional list of domains to restrict results to (web-search connector + * only) and advance. An empty submission clears the field. + */ + const setIncludeDomains = useCallback( + (includeDomains: string[] | undefined) => { + setConfig(c => ({ + ...c, + includeDomains: includeDomains && includeDomains.length > 0 ? includeDomains : undefined, + })); + goToNextStep(); + }, + [goToNextStep] + ); + /** * Set the optional list of domains to exclude (web-search connector only) * and advance to confirm. An empty submission clears the field. @@ -392,6 +407,7 @@ export function useAddGatewayTargetWizard( setStickinessConfig, setSigningService, setSigningRegion, + setIncludeDomains, setExcludeDomains, reset, };