Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/cli/commands/add/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 10 additions & 6 deletions src/cli/commands/add/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,12 +437,16 @@
}
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
Expand Down Expand Up @@ -699,7 +703,7 @@
if (!passthroughEndpoint) {
return { valid: false, error: '--passthrough-endpoint is required for passthrough type' };
}
if (!/^https:\/\/[a-zA-Z0-9\-.]+(:[0-9]{1,5})?(\/.*)?$/.test(passthroughEndpoint)) {

Check warning on line 706 in src/cli/commands/add/validate.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe Regular Expression
return { valid: false, error: '--passthrough-endpoint must be a valid HTTPS URL' };
}
if (options.language && options.language !== 'Other') {
Expand Down
48 changes: 48 additions & 0 deletions src/cli/operations/connectors/__tests__/translators.test.ts
Original file line number Diff line number Diff line change
@@ -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'] },
});
});
});
14 changes: 13 additions & 1 deletion src/cli/operations/connectors/translators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface ParameterOverride {
}

export interface WebSearchTranslatorInput {
includeDomains?: string[];
excludeDomains?: string[];
}

Expand All @@ -33,9 +34,20 @@ export type ConnectorTranslatorInput =

function translateWebSearch(input: WebSearchTranslatorInput): ConfigurationEntry[] {
const parameterValues: Record<string, unknown> = {};

// 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: [] }];
}

Expand Down
31 changes: 23 additions & 8 deletions src/cli/primitives/GatewayTargetPrimitive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,16 +277,18 @@ export class GatewayTargetPrimitive extends BasePrimitive<AddGatewayTargetOption
const typeDescription =
'Target type (required): mcp-server, api-gateway, open-api-schema, smithy-model, lambda-function-arn, http-runtime, connector, passthrough [non-interactive]';

// Reject repeated use of --exclude-domains. Domains must be passed as a
// Reject repeated use of the domain filters. Domains must be passed as a
// single comma-separated value.
const excludeDomainsCoercer = (val: string, prev?: string) => {
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')
Expand All @@ -305,6 +307,11 @@ export class GatewayTargetPrimitive extends BasePrimitive<AddGatewayTargetOption
(val: string, acc: string[]) => [...acc, val],
[] as string[]
)
.option(
'--include-domains <list>',
'Comma-separated domains to restrict results to (for --connector web-search) [non-interactive]',
includeDomainsCoercer
)
.option(
'--exclude-domains <list>',
'Comma-separated domains to exclude from results (for --connector web-search) [non-interactive]',
Expand Down Expand Up @@ -411,6 +418,7 @@ Target types and their options:
connector — Wire a managed AWS connector (bedrock-knowledge-bases, web-search)
--connector <id> bedrock-knowledge-bases or web-search
--knowledge-base-id <id> Project KB name or 10-char external KB id (for KB connectors)
--include-domains <list> Comma-separated domains to restrict results to (for web-search connector)
--exclude-domains <list> Comma-separated domains to exclude (for web-search connector)

passthrough — Route to an external HTTPS endpoint
Expand Down Expand Up @@ -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 };
Expand Down Expand Up @@ -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 = {
Expand Down
32 changes: 32 additions & 0 deletions src/cli/primitives/__tests__/GatewayTargetPrimitive.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
23 changes: 22 additions & 1 deletion src/cli/tui/screens/mcp/AddGatewayTargetScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -812,6 +814,22 @@ export function AddGatewayTargetScreen({
/>
)}

{isIncludeDomainsStep && (
<TextInput
prompt="Restrict to domains (optional, comma-separated)"
placeholder="e.g. docs.aws.amazon.com, aws.amazon.com"
allowEmpty
onSubmit={(value: string) => {
const domains = value
.split(',')
.map(d => d.trim())
.filter(d => d.length > 0);
wizard.setIncludeDomains(domains.length > 0 ? domains : undefined);
}}
onCancel={() => wizard.goBack()}
/>
)}

{isExcludeDomainsStep && (
<TextInput
prompt="Exclude domains (optional, comma-separated)"
Expand Down Expand Up @@ -865,7 +883,10 @@ export function AddGatewayTargetScreen({
]
: []),
...(wizard.config.connectorId === 'web-search'
? [{ label: 'Exclude domains', value: wizard.config.excludeDomains?.join(', ') ?? '(none)' }]
? [
{ label: 'Include domains', value: wizard.config.includeDomains?.join(', ') ?? '(none)' },
{ label: 'Exclude domains', value: wizard.config.excludeDomains?.join(', ') ?? '(none)' },
]
: []),
...(wizard.config.targetType === 'connector' && wizard.config.connectorId !== 'web-search'
? [
Expand Down
6 changes: 6 additions & 0 deletions src/cli/tui/screens/mcp/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export type AddGatewayTargetStep =
| 'passthrough-stickiness'
| 'signing-service'
| 'signing-region'
| 'include-domains'
| 'exclude-domains'
| 'confirm';

Expand Down Expand Up @@ -159,6 +160,8 @@ export interface GatewayTargetWizardState {
signingService?: string;
/** SigV4 signing region for passthrough GATEWAY_IAM_ROLE auth */
signingRegion?: string;
/** Optional list of domains to restrict results to (webSearch target type only). */
includeDomains?: string[];
/** Optional list of domains to exclude (webSearch target type only). */
excludeDomains?: string[];
}
Expand Down Expand Up @@ -263,6 +266,8 @@ export interface WebSearchTargetConfig {
targetType: 'webSearch';
name: string;
gateway: string;
/** Optional list of domains to restrict web search results to. */
includeDomains?: string[];
/** Optional list of domains to exclude from web search results. */
excludeDomains?: string[];
}
Expand Down Expand Up @@ -301,6 +306,7 @@ export const MCP_TOOL_STEP_LABELS: Record<AddGatewayTargetStep, string> = {
'passthrough-stickiness': 'Stickiness',
'signing-service': 'Signing Service',
'signing-region': 'Signing Region',
'include-domains': 'Include Domains',
'exclude-domains': 'Exclude Domains',
confirm: 'Confirm',
};
Expand Down
20 changes: 18 additions & 2 deletions src/cli/tui/screens/mcp/useAddGatewayTargetWizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand Down Expand Up @@ -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':
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -392,6 +407,7 @@ export function useAddGatewayTargetWizard(
setStickinessConfig,
setSigningService,
setSigningRegion,
setIncludeDomains,
setExcludeDomains,
reset,
};
Expand Down
Loading