diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index ea6623ee9..ddd981e16 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -7430,7 +7430,7 @@ file maps to a JSON config file and includes validation constraints as comments - **NetworkMode**: \`'PUBLIC'\` | \`'VPC'\` - **RuntimeVersion**: \`'PYTHON_3_10'\` | \`'PYTHON_3_11'\` | \`'PYTHON_3_12'\` | \`'PYTHON_3_13'\` | \`'PYTHON_3_14'\` | \`'NODE_18'\` | \`'NODE_20'\` | \`'NODE_22'\` - **MemoryStrategyType**: \`'SEMANTIC'\` | \`'SUMMARIZATION'\` | \`'USER_PREFERENCE'\` | \`'EPISODIC'\` -- **GatewayTargetType**: \`'lambda'\` | \`'mcpServer'\` | \`'openApiSchema'\` | \`'smithyModel'\` | \`'apiGateway'\` | \`'lambdaFunctionArn'\` | \`'connector'\` (web-search, bedrock-knowledge-bases, bedrock-agentic-retrieve) +- **GatewayTargetType**: \`'lambda'\` | \`'mcpServer'\` | \`'openApiSchema'\` | \`'smithyModel'\` | \`'apiGateway'\` | \`'lambdaFunctionArn'\` | \`'connector'\` (web-search, bedrock-knowledge-bases) - **ModelProvider**: \`'Bedrock'\` | \`'Gemini'\` | \`'OpenAI'\` | \`'Anthropic'\` - **PaymentProvider**: \`'CoinbaseCDP'\` | \`'StripePrivy'\` - **PolicyEnforcementMode**: \`'ACTIVE'\` | \`'PASSIVE'\` diff --git a/src/assets/agents/AGENTS.md b/src/assets/agents/AGENTS.md index 7fd817773..53877021e 100644 --- a/src/assets/agents/AGENTS.md +++ b/src/assets/agents/AGENTS.md @@ -78,7 +78,7 @@ file maps to a JSON config file and includes validation constraints as comments - **NetworkMode**: `'PUBLIC'` | `'VPC'` - **RuntimeVersion**: `'PYTHON_3_10'` | `'PYTHON_3_11'` | `'PYTHON_3_12'` | `'PYTHON_3_13'` | `'PYTHON_3_14'` | `'NODE_18'` | `'NODE_20'` | `'NODE_22'` - **MemoryStrategyType**: `'SEMANTIC'` | `'SUMMARIZATION'` | `'USER_PREFERENCE'` | `'EPISODIC'` -- **GatewayTargetType**: `'lambda'` | `'mcpServer'` | `'openApiSchema'` | `'smithyModel'` | `'apiGateway'` | `'lambdaFunctionArn'` | `'connector'` (web-search, bedrock-knowledge-bases, bedrock-agentic-retrieve) +- **GatewayTargetType**: `'lambda'` | `'mcpServer'` | `'openApiSchema'` | `'smithyModel'` | `'apiGateway'` | `'lambdaFunctionArn'` | `'connector'` (web-search, bedrock-knowledge-bases) - **ModelProvider**: `'Bedrock'` | `'Gemini'` | `'OpenAI'` | `'Anthropic'` - **PaymentProvider**: `'CoinbaseCDP'` | `'StripePrivy'` - **PolicyEnforcementMode**: `'ACTIVE'` | `'PASSIVE'` diff --git a/src/cli/commands/add/types.ts b/src/cli/commands/add/types.ts index 49ae70798..afa34c2b3 100644 --- a/src/cli/commands/add/types.ts +++ b/src/cli/commands/add/types.ts @@ -92,13 +92,12 @@ export interface AddGatewayTargetOptions { schemaS3Account?: string; runtime?: string; runtimeEndpoint?: string; - /** Connector id (for --type connector): bedrock-knowledge-bases | bedrock-agentic-retrieve | web-search. */ + /** Connector id (for --type connector): bedrock-knowledge-bases | web-search. */ connector?: string; /** * KB reference for --type connector — either a project KB name (entry in - * knowledgeBases[]) or a literal 10-char external KB ID. Repeatable when - * --connector is bedrock-agentic-retrieve (fan-out); single-valued for - * bedrock-knowledge-bases. Not applicable to --connector web-search. + * knowledgeBases[]) or a literal 10-char external KB ID. Not applicable to + * --connector web-search. */ knowledgeBaseId?: string[]; passthroughEndpoint?: string; diff --git a/src/cli/commands/add/validate.ts b/src/cli/commands/add/validate.ts index ca5910293..3b027fdb2 100644 --- a/src/cli/commands/add/validate.ts +++ b/src/cli/commands/add/validate.ts @@ -394,7 +394,9 @@ export async function validateAddGatewayTargetOptions(options: AddGatewayTargetO options.language = (matchEnumValue(TargetLanguageSchema, options.language) as typeof options.language) ?? options.language; - if (!options.name) { + const kbConnectors = ['bedrock-knowledge-bases']; + const nameOptional = options.type === 'connector' && kbConnectors.includes(options.connector ?? ''); + if (!options.name && !nameOptional) { return { valid: false, error: '--name is required' }; } @@ -407,7 +409,6 @@ export async function validateAddGatewayTargetOptions(options: AddGatewayTargetO 'http-runtime', 'connector', 'passthrough', - 'web-search', ].join(', '); if (!options.type) { @@ -426,7 +427,6 @@ export async function validateAddGatewayTargetOptions(options: AddGatewayTargetO 'http-runtime': 'httpRuntime', connector: 'connector', passthrough: 'passthrough', - 'web-search': 'webSearch', }; const mappedType = typeMap[options.type]; if (!mappedType) { @@ -437,11 +437,11 @@ export async function validateAddGatewayTargetOptions(options: AddGatewayTargetO } options.type = mappedType; - // --exclude-domains is webSearch-target-only. Reject it on every other target type. - if (mappedType !== 'webSearch' && options.excludeDomains) { + // --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 --type web-search', + error: '--exclude-domains only applies to --connector web-search', }; } @@ -630,43 +630,6 @@ export async function validateAddGatewayTargetOptions(options: AddGatewayTargetO return { valid: true }; } - // Web search targets (Amazon Web Search managed connector): validate early and return - if (mappedType === 'webSearch') { - const WEB_SEARCH_DISALLOWED_OPTIONS: [string, string][] = [ - ['connector', '--connector'], - ['knowledgeBaseId', '--knowledge-base-id'], - ['endpoint', '--endpoint'], - ['host', '--host'], - ['restApiId', '--rest-api-id'], - ['stage', '--stage'], - ['lambdaArn', '--lambda-arn'], - ['toolSchemaFile', '--tool-schema-file'], - ['toolFilterPath', '--tool-filter-path'], - ['toolFilterMethods', '--tool-filter-methods'], - ['schema', '--schema'], - ['schemaS3Account', '--schema-s3-account'], - ['outboundAuthType', '--outbound-auth'], - ['credentialName', '--credential-name'], - ['oauthClientId', '--oauth-client-id'], - ['oauthClientSecret', '--oauth-client-secret'], - ['oauthDiscoveryUrl', '--oauth-discovery-url'], - ['oauthScopes', '--oauth-scopes'], - ]; - for (const [key, flag] of WEB_SEARCH_DISALLOWED_OPTIONS) { - const v = (options as unknown as Record)[key]; - // knowledgeBaseId is a string[]; treat empty array as absent - const present = Array.isArray(v) ? v.length > 0 : !!v; - if (present) { - return { valid: false, error: `${flag} is not applicable for web-search type` }; - } - } - if (options.language && options.language !== 'Other') { - return { valid: false, error: '--language is not applicable for web-search type' }; - } - options.language = 'Other'; - return { valid: true }; - } - // Connector targets (Bedrock KB, agentic-retrieve): validate early and return if (mappedType === 'connector') { const validConnectors = CONNECTOR_ID_VALUES.join(', '); @@ -682,17 +645,22 @@ export async function validateAddGatewayTargetOptions(options: AddGatewayTargetO error: `Invalid --connector "${options.connector}". Valid: ${validConnectors}`, }; } - if (!options.knowledgeBaseId || options.knowledgeBaseId.length === 0) { + if (options.connector === 'web-search' && options.knowledgeBaseId && options.knowledgeBaseId.length > 0) { + return { + valid: false, + error: '--knowledge-base-id is not applicable for --connector web-search', + }; + } + if (options.connector !== 'web-search' && (!options.knowledgeBaseId || options.knowledgeBaseId.length === 0)) { return { valid: false, error: `--knowledge-base-id is required for --connector ${options.connector}`, }; } - if (options.connector === 'bedrock-knowledge-bases' && options.knowledgeBaseId.length > 1) { + if (options.connector === 'bedrock-knowledge-bases' && (options.knowledgeBaseId?.length ?? 0) > 1) { return { valid: false, - error: - '--knowledge-base-id may only be specified once for --connector bedrock-knowledge-bases. Use --connector bedrock-agentic-retrieve for fan-out.', + error: '--knowledge-base-id may only be specified once for --connector bedrock-knowledge-bases.', }; } const irrelevant: [string, string][] = [ @@ -704,6 +672,8 @@ export async function validateAddGatewayTargetOptions(options: AddGatewayTargetO ['toolSchemaFile', '--tool-schema-file'], ['toolFilterPath', '--tool-filter-path'], ['toolFilterMethods', '--tool-filter-methods'], + ['schema', '--schema'], + ['schemaS3Account', '--schema-s3-account'], ['outboundAuthType', '--outbound-auth'], ['credentialName', '--credential-name'], ['oauthClientId', '--oauth-client-id'], diff --git a/src/cli/commands/status/__tests__/action.test.ts b/src/cli/commands/status/__tests__/action.test.ts index 42af29bc0..faceb2812 100644 --- a/src/cli/commands/status/__tests__/action.test.ts +++ b/src/cli/commands/status/__tests__/action.test.ts @@ -626,7 +626,7 @@ describe('computeResourceStatuses', () => { name: 'docs', targetType: 'connector', connectorId: 'bedrock-knowledge-bases', - knowledgeBaseId: 'docs', + configurations: [{ name: 'Retrieve', parameterValues: { knowledgeBaseId: 'docs' } }], }, ], }, @@ -653,7 +653,18 @@ describe('computeResourceStatuses', () => { name: 'main-gw', targets: [ { name: 't1', targetType: 'mcpServer' }, - { name: 'docs', targetType: 'connector', connectorId: 'bedrock-knowledge-bases', knowledgeBaseId: 'docs' }, + { + name: 'docs', + targetType: 'connector', + connectorId: 'bedrock-knowledge-bases', + configurations: [ + { name: 'Retrieve', parameterValues: { knowledgeBaseId: 'docs' } }, + { + name: 'AgenticRetrieveStream', + parameterValues: { retrievers: [{ configuration: { knowledgeBase: { knowledgeBaseId: 'docs' } } }] }, + }, + ], + }, ], }, ], @@ -661,7 +672,7 @@ describe('computeResourceStatuses', () => { const result = computeResourceStatuses(project, undefined); const gwEntry = result.find(r => r.resourceType === 'gateway' && r.name === 'main-gw'); - expect(gwEntry?.detail).toBe('2 targets (1 retrieve)'); + expect(gwEntry?.detail).toBe('2 targets (1 retrieve, agentic ×1)'); }); it('annotates gateway detail with both retrieve count and agentic fan-out', () => { @@ -671,13 +682,29 @@ describe('computeResourceStatuses', () => { { name: 'main-gw', targets: [ - { name: 'docs', targetType: 'connector', connectorId: 'bedrock-knowledge-bases', knowledgeBaseId: 'docs' }, - { name: 'hr', targetType: 'connector', connectorId: 'bedrock-knowledge-bases', knowledgeBaseId: 'hr' }, { - name: 'main-gw-agentic', + name: 'docs', + targetType: 'connector', + connectorId: 'bedrock-knowledge-bases', + configurations: [ + { name: 'Retrieve', parameterValues: { knowledgeBaseId: 'docs' } }, + { + name: 'AgenticRetrieveStream', + parameterValues: { retrievers: [{ configuration: { knowledgeBase: { knowledgeBaseId: 'docs' } } }] }, + }, + ], + }, + { + name: 'hr', targetType: 'connector', - connectorId: 'bedrock-agentic-retrieve', - knowledgeBaseIds: ['docs', 'hr'], + connectorId: 'bedrock-knowledge-bases', + configurations: [ + { name: 'Retrieve', parameterValues: { knowledgeBaseId: 'hr' } }, + { + name: 'AgenticRetrieveStream', + parameterValues: { retrievers: [{ configuration: { knowledgeBase: { knowledgeBaseId: 'hr' } } }] }, + }, + ], }, ], }, @@ -686,10 +713,10 @@ describe('computeResourceStatuses', () => { const result = computeResourceStatuses(project, undefined); const gwEntry = result.find(r => r.resourceType === 'gateway' && r.name === 'main-gw'); - expect(gwEntry?.detail).toBe('3 targets (2 retrieve, agentic ×2)'); + expect(gwEntry?.detail).toBe('2 targets (2 retrieve, agentic ×2)'); }); - it('KB detail surfaces wiring from agentic-retrieve fan-out target', () => { + it('KB detail surfaces wiring from configurations', () => { const project = { ...baseProject, agentCoreGateways: [ @@ -697,10 +724,16 @@ describe('computeResourceStatuses', () => { name: 'main-gw', targets: [ { - name: 'main-gw-agentic', + name: 'docs-target', targetType: 'connector', - connectorId: 'bedrock-agentic-retrieve', - knowledgeBaseIds: ['docs'], + connectorId: 'bedrock-knowledge-bases', + configurations: [ + { name: 'Retrieve', parameterValues: { knowledgeBaseId: 'docs' } }, + { + name: 'AgenticRetrieveStream', + parameterValues: { retrievers: [{ configuration: { knowledgeBase: { knowledgeBaseId: 'docs' } } }] }, + }, + ], }, ], }, @@ -929,7 +962,7 @@ describe('handleProjectStatus — knowledge base enrichment', () => { name: 'docs', targetType: 'connector', connectorId: 'bedrock-knowledge-bases', - knowledgeBaseId: 'product-docs', + configurations: [{ name: 'Retrieve', parameterValues: { knowledgeBaseId: 'product-docs' } }], }, ], }, diff --git a/src/cli/commands/status/action.ts b/src/cli/commands/status/action.ts index bc1eb2002..12bccd24d 100644 --- a/src/cli/commands/status/action.ts +++ b/src/cli/commands/status/action.ts @@ -172,15 +172,21 @@ export function computeResourceStatuses( const retrieveCount = targets.filter( t => t.targetType === 'connector' && t.connectorId === 'bedrock-knowledge-bases' ).length; - const agentic = targets.find(t => t.targetType === 'connector' && t.connectorId === 'bedrock-agentic-retrieve'); - const webSearchCount = targets.filter(t => t.targetType === 'webSearch').length; + const webSearchCount = targets.filter(t => t.targetType === 'connector' && t.connectorId === 'web-search').length; const base = `${targets.length} target${targets.length !== 1 ? 's' : ''}`; const parts: string[] = []; if (retrieveCount > 0) parts.push(`${retrieveCount} retrieve`); - if (agentic) { - const fanOut = agentic.knowledgeBaseIds?.length ?? 0; - parts.push(`agentic ×${fanOut}`); + // Count agentic retrievers from AgenticRetrieveStream configurations within KB connector targets + let agenticFanOut = 0; + for (const t of targets) { + if (t.targetType !== 'connector' || t.connectorId !== 'bedrock-knowledge-bases') continue; + const agenticConfig = (t.configurations ?? []).find(c => c.name === 'AgenticRetrieveStream'); + if (agenticConfig) { + const retrievers = agenticConfig.parameterValues?.retrievers as unknown[] | undefined; + agenticFanOut += retrievers?.length ?? 0; + } } + if (agenticFanOut > 0) parts.push(`agentic ×${agenticFanOut}`); if (webSearchCount > 0) parts.push(`${webSearchCount} web-search`); return parts.length > 0 ? `${base} (${parts.join(', ')})` : base; }, @@ -248,9 +254,8 @@ export function computeResourceStatuses( }); // Reverse-index: KB name -> list of gateways with a connector target referencing it. - // Walks both knowledgeBaseId (single-KB Retrieve) and knowledgeBaseIds[] - // (agentic-retrieve fan-out) so a KB shows its wiring no matter which - // connector kind references it. + // Walks both Retrieve (knowledgeBaseId) and AgenticRetrieveStream (retrievers[]) + // configurations so a KB shows its wiring no matter which configuration references it. const kbToGateways = new Map>(); const recordKbWiring = (kbRef: string, gatewayName: string): void => { const set = kbToGateways.get(kbRef) ?? new Set(); @@ -260,8 +265,22 @@ export function computeResourceStatuses( for (const gw of project.agentCoreGateways ?? []) { for (const t of gw.targets ?? []) { if (t.targetType !== 'connector') continue; - if (t.knowledgeBaseId) recordKbWiring(t.knowledgeBaseId, gw.name); - for (const ref of t.knowledgeBaseIds ?? []) recordKbWiring(ref, gw.name); + for (const cfg of t.configurations ?? []) { + const pv = cfg.parameterValues; + if (cfg.name === 'Retrieve') { + const kbId = pv?.knowledgeBaseId as string | undefined; + if (kbId) recordKbWiring(kbId, gw.name); + } + if (cfg.name === 'AgenticRetrieveStream') { + const retrievers = pv?.retrievers as + | { configuration?: { knowledgeBase?: { knowledgeBaseId?: string } } }[] + | undefined; + for (const r of retrievers ?? []) { + const ref = r?.configuration?.knowledgeBase?.knowledgeBaseId; + if (ref) recordKbWiring(ref, gw.name); + } + } + } } } @@ -532,10 +551,9 @@ export async function handleProjectStatus( logger.startStep(`Fetch knowledge base status (${deployedKbs.length} KB${deployedKbs.length !== 1 ? 's' : ''})`); // Reverse-index: KB spec name -> gateways whose connector targets - // reference it. Project-owned KBs are stored by *name* on connector - // targets (single-KB Retrieve on `knowledgeBaseId`, agentic-retrieve - // fan-out on `knowledgeBaseIds[]`), so we key by the spec name - // (entry.name) below. + // reference it. Project-owned KBs are stored by *name* in connector + // target configurations (Retrieve on `knowledgeBaseId`, AgenticRetrieveStream + // on `retrievers[]`), so we key by the spec name (entry.name) below. const kbNameToGateways = new Map>(); const recordKbWiring = (kbRef: string, gatewayName: string): void => { const set = kbNameToGateways.get(kbRef) ?? new Set(); @@ -545,8 +563,22 @@ export async function handleProjectStatus( for (const gw of project.agentCoreGateways ?? []) { for (const t of gw.targets ?? []) { if (t.targetType !== 'connector') continue; - if (t.knowledgeBaseId) recordKbWiring(t.knowledgeBaseId, gw.name); - for (const ref of t.knowledgeBaseIds ?? []) recordKbWiring(ref, gw.name); + for (const cfg of t.configurations ?? []) { + const pv = cfg.parameterValues; + if (cfg.name === 'Retrieve') { + const kbId = pv?.knowledgeBaseId as string | undefined; + if (kbId) recordKbWiring(kbId, gw.name); + } + if (cfg.name === 'AgenticRetrieveStream') { + const retrievers = pv?.retrievers as + | { configuration?: { knowledgeBase?: { knowledgeBaseId?: string } } }[] + | undefined; + for (const r of retrievers ?? []) { + const ref = r?.configuration?.knowledgeBase?.knowledgeBaseId; + if (ref) recordKbWiring(ref, gw.name); + } + } + } } } diff --git a/src/cli/operations/connectors/index.ts b/src/cli/operations/connectors/index.ts new file mode 100644 index 000000000..6a64f1e86 --- /dev/null +++ b/src/cli/operations/connectors/index.ts @@ -0,0 +1,8 @@ +export { translateConnector } from './translators'; +export type { + ConfigurationEntry, + ConnectorTranslatorInput, + ParameterOverride, + WebSearchTranslatorInput, + KnowledgeBasesTranslatorInput, +} from './translators'; diff --git a/src/cli/operations/connectors/translators.ts b/src/cli/operations/connectors/translators.ts new file mode 100644 index 000000000..9daf29b3e --- /dev/null +++ b/src/cli/operations/connectors/translators.ts @@ -0,0 +1,66 @@ +/** + * Connector translators: convert validated CLI inputs into the configurations[] + * array expected by the on-disk schema (which mirrors the CFN wire format). + * + * Each connector exposes one or more operations (e.g. "Retrieve", "AgenticRetrieveStream", + * "WebSearch"). The translator produces a configuration entry per operation. + */ + +export interface ConfigurationEntry { + name: string; + parameterValues: Record; + parameterOverrides: ParameterOverride[]; +} + +export interface ParameterOverride { + path: string; + description?: string; + visible?: boolean; +} + +export interface WebSearchTranslatorInput { + excludeDomains?: string[]; +} + +export interface KnowledgeBasesTranslatorInput { + knowledgeBaseId: string; +} + +export type ConnectorTranslatorInput = + | { connectorId: 'web-search'; input: WebSearchTranslatorInput } + | { connectorId: 'bedrock-knowledge-bases'; input: KnowledgeBasesTranslatorInput }; + +function translateWebSearch(input: WebSearchTranslatorInput): ConfigurationEntry[] { + const parameterValues: Record = {}; + if (input.excludeDomains && input.excludeDomains.length > 0) { + parameterValues.domainFilter = { exclude: input.excludeDomains }; + } + return [{ name: 'WebSearch', parameterValues, parameterOverrides: [] }]; +} + +function translateKnowledgeBases(input: KnowledgeBasesTranslatorInput): ConfigurationEntry[] { + return [ + { + name: 'AgenticRetrieveStream', + parameterValues: { + retrievers: [{ configuration: { knowledgeBase: { knowledgeBaseId: input.knowledgeBaseId } } }], + agenticRetrieveConfiguration: { foundationModelType: 'MANAGED', rerankingModelType: 'MANAGED' }, + }, + parameterOverrides: [], + }, + { + name: 'Retrieve', + parameterValues: { knowledgeBaseId: input.knowledgeBaseId }, + parameterOverrides: [], + }, + ]; +} + +export function translateConnector(args: ConnectorTranslatorInput): ConfigurationEntry[] { + switch (args.connectorId) { + case 'web-search': + return translateWebSearch(args.input); + case 'bedrock-knowledge-bases': + return translateKnowledgeBases(args.input); + } +} diff --git a/src/cli/operations/knowledge-base/__tests__/agentic-retrieve-upsert.test.ts b/src/cli/operations/knowledge-base/__tests__/agentic-retrieve-upsert.test.ts deleted file mode 100644 index 8d2ab8248..000000000 --- a/src/cli/operations/knowledge-base/__tests__/agentic-retrieve-upsert.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { AgentCoreProjectSpec } from '../../../../schema'; -import { upsertAgenticRetrieveTarget } from '../agentic-retrieve-upsert'; -import { describe, expect, it } from 'vitest'; - -type Gateway = AgentCoreProjectSpec['agentCoreGateways'][number]; - -function makeGateway(name = 'main-gw'): Gateway { - return { - name, - targets: [], - authorizerType: 'NONE', - enableSemanticSearch: true, - exceptionLevel: 'NONE', - } as unknown as Gateway; -} - -describe('upsertAgenticRetrieveTarget', () => { - it('creates a new agentic-retrieve target on first call', () => { - const gw = makeGateway(); - upsertAgenticRetrieveTarget(gw, 'kb-1'); - expect(gw.targets).toHaveLength(1); - const target = gw.targets[0]; - expect(target?.name).toBe('main-gw-agentic'); - expect(target?.targetType).toBe('connector'); - expect(target?.connectorId).toBe('bedrock-agentic-retrieve'); - expect(target?.knowledgeBaseIds).toEqual(['kb-1']); - }); - - it('appends to an existing agentic-retrieve target', () => { - const gw = makeGateway(); - upsertAgenticRetrieveTarget(gw, 'kb-1'); - upsertAgenticRetrieveTarget(gw, 'kb-2'); - expect(gw.targets).toHaveLength(1); - expect(gw.targets[0]?.knowledgeBaseIds).toEqual(['kb-1', 'kb-2']); - }); - - it('is idempotent — re-adding the same kb is a no-op', () => { - const gw = makeGateway(); - upsertAgenticRetrieveTarget(gw, 'kb-1'); - upsertAgenticRetrieveTarget(gw, 'kb-1'); - expect(gw.targets).toHaveLength(1); - expect(gw.targets[0]?.knowledgeBaseIds).toEqual(['kb-1']); - }); - - it('respects a hand-renamed agentic target and only appends', () => { - const gw = makeGateway(); - gw.targets.push({ - name: 'custom-name', - targetType: 'connector', - connectorId: 'bedrock-agentic-retrieve', - knowledgeBaseIds: ['existing-kb'], - } as unknown as Gateway['targets'][number]); - - upsertAgenticRetrieveTarget(gw, 'new-kb'); - expect(gw.targets).toHaveLength(1); - expect(gw.targets[0]?.name).toBe('custom-name'); - expect(gw.targets[0]?.knowledgeBaseIds).toEqual(['existing-kb', 'new-kb']); - }); -}); diff --git a/src/cli/operations/knowledge-base/agentic-retrieve-upsert.ts b/src/cli/operations/knowledge-base/agentic-retrieve-upsert.ts deleted file mode 100644 index 10702b31d..000000000 --- a/src/cli/operations/knowledge-base/agentic-retrieve-upsert.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { AgentCoreGatewayTarget, AgentCoreProjectSpec } from '../../../schema'; -import { CONNECTOR_ID } from '../../../schema'; - -/** - * Ensure exactly one bedrock-agentic-retrieve target exists on this gateway, - * with kbReference present in its knowledgeBaseIds[]. Idempotent. - * - * - Creates the target on first call (named `${gateway.name}-agentic`). - * - Appends to it on subsequent calls if kbReference is missing. - * - No-op if kbReference is already in the agentic target's knowledgeBaseIds[]. - * - * Mutates `gateway.targets` in place. Used by both KnowledgeBasePrimitive - * (project-owned KBs via `add knowledge-base --gateway`) and - * GatewayTargetPrimitive (external KBs via `add gateway-target --type - * connector --connector bedrock-knowledge-bases`) so wiring is consistent - * across paths. - * - * If the user has hand-renamed the agentic target, we respect it and only - * append; we don't rename it back. - */ -export function upsertAgenticRetrieveTarget( - gateway: AgentCoreProjectSpec['agentCoreGateways'][number], - kbReference: string -): void { - const existing = gateway.targets.find( - t => t.targetType === 'connector' && t.connectorId === CONNECTOR_ID.BEDROCK_AGENTIC_RETRIEVE - ); - if (existing) { - const ids = existing.knowledgeBaseIds ?? []; - if (!ids.includes(kbReference)) { - existing.knowledgeBaseIds = [...ids, kbReference]; - } - return; - } - const agenticTarget: AgentCoreGatewayTarget = { - name: `${gateway.name}-agentic`, - targetType: 'connector', - connectorId: CONNECTOR_ID.BEDROCK_AGENTIC_RETRIEVE, - knowledgeBaseIds: [kbReference], - } as AgentCoreGatewayTarget; - gateway.targets.push(agenticTarget); -} diff --git a/src/cli/primitives/GatewayTargetPrimitive.ts b/src/cli/primitives/GatewayTargetPrimitive.ts index 55df816a9..78203ea20 100644 --- a/src/cli/primitives/GatewayTargetPrimitive.ts +++ b/src/cli/primitives/GatewayTargetPrimitive.ts @@ -29,8 +29,7 @@ import { import type { AddGatewayTargetOptions as CLIAddGatewayTargetOptions } from '../commands/add/types'; import { validateAddGatewayTargetOptions } from '../commands/add/validate'; import { getErrorMessage } from '../errors'; -import { isGatedFeaturesEnabled } from '../feature-flags.js'; -import { upsertAgenticRetrieveTarget } from '../operations/knowledge-base/agentic-retrieve-upsert'; +import { translateConnector } from '../operations/connectors'; import type { RemovableGatewayTarget } from '../operations/remove/remove-gateway-target'; import type { RemovalPreview, SchemaChange } from '../operations/remove/types'; import { runCliCommand, withCommandRunTelemetry } from '../telemetry/cli-command-run.js'; @@ -275,12 +274,8 @@ export class GatewayTargetPrimitive extends BasePrimitive(option: T): T => (isGatedFeaturesEnabled() ? option : option.hideHelp()); - - const typeDescription = isGatedFeaturesEnabled() - ? 'Target type (required): mcp-server, api-gateway, open-api-schema, smithy-model, lambda-function-arn, http-runtime, connector, passthrough, web-search [non-interactive]' - : 'Target type (required): mcp-server, api-gateway, open-api-schema, smithy-model, lambda-function-arn, http-runtime, connector, passthrough [non-interactive]'; + 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 // single comma-separated value. @@ -302,21 +297,18 @@ export class GatewayTargetPrimitive extends BasePrimitive', typeDescription) .option( '--connector ', - 'Connector id (for connector type): bedrock-knowledge-bases or bedrock-agentic-retrieve [non-interactive]' + 'Connector id (for connector type): bedrock-knowledge-bases or web-search [non-interactive]' ) .option( '--knowledge-base-id ', - 'KB reference for connector type — either a project KB name (entry in knowledgeBases[]) or a 10-char Bedrock KB id for an external KB. Repeatable for --connector bedrock-agentic-retrieve to fan out across multiple KBs. [non-interactive]', + 'KB reference for connector type — either a project KB name (entry in knowledgeBases[]) or a 10-char Bedrock KB id for an external KB. [non-interactive]', (val: string, acc: string[]) => [...acc, val], [] as string[] ) - .addOption( - gate( - new Option( - '--exclude-domains ', - 'Comma-separated domains to exclude from results (for --type web-search only) [non-interactive]' - ).argParser(excludeDomainsCoercer) - ) + .option( + '--exclude-domains ', + 'Comma-separated domains to exclude from results (for --connector web-search) [non-interactive]', + excludeDomainsCoercer ) .option('--endpoint ', 'Server endpoint URL (for mcp-server type) [non-interactive]') .option('--language ', 'Language of target code: Python, TypeScript, Other [non-interactive]') @@ -413,9 +405,10 @@ Target types and their options: --lambda-arn Lambda function ARN --tool-schema-file Tool schema JSON file - connector — Wire a managed AWS connector (Bedrock KB, agentic-retrieve) - --connector bedrock-knowledge-bases or bedrock-agentic-retrieve - --knowledge-base-id Project KB name or 10-char external KB id (repeatable for agentic-retrieve) + 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) + --exclude-domains Comma-separated domains to exclude (for web-search connector) passthrough — Route to an external HTTPS endpoint --passthrough-endpoint HTTPS endpoint URL @@ -439,6 +432,35 @@ Target types and their options: process.exit(1); } + const userPassedAnyFlag = Object.entries(rawOptions).some( + ([, v]) => v !== undefined && v !== false && !(Array.isArray(v) && v.length === 0) + ); + if (!userPassedAnyFlag) { + try { + requireTTY(); + const [{ render }, { default: React }, { AddFlow }] = await Promise.all([ + import('ink'), + import('react'), + import('../tui/screens/add/AddFlow'), + ]); + const { clear, unmount } = render( + React.createElement(AddFlow, { + isInteractive: false, + initialResource: 'gateway-target', + onExit: () => { + clear(); + unmount(); + process.exit(0); + }, + }) + ); + return; + } catch (error) { + console.error(getErrorMessage(error)); + process.exit(1); + } + } + await runCliCommand('add.gateway-target', !!cliOptions.json, async () => { const validation = await validateAddGatewayTargetOptions(cliOptions); if (!validation.valid) { @@ -593,35 +615,7 @@ Target types and their options: return telemetryAttrs; } - // Handle Amazon Web Search targets (managed-service backed via gateway IAM role) - if (cliOptions.type === 'webSearch') { - if (!isGatedFeaturesEnabled()) { - throw new ValidationError('Web search target type is not yet available.'); - } - const excludeDomains = - typeof cliOptions.excludeDomains === 'string' - ? cliOptions.excludeDomains - .split(',') - .map((d: string) => d.trim()) - .filter((d: string) => d.length > 0) - : undefined; - const config: WebSearchTargetConfig = { - targetType: 'webSearch', - name: cliOptions.name!, - gateway: cliOptions.gateway!, - ...(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(',')})` : ''; - console.log(`Added web-search gateway target '${result.toolName}' on '${config.gateway}'${suffix}`); - } - return telemetryAttrs; - } - - // Handle connector targets (managed-service backed: KB single-retrieve, agentic-retrieve fan-out) + // Handle connector targets (managed-service backed: KB, web-search) if (cliOptions.type === 'connector') { const validConnectors = CONNECTOR_ID_VALUES.join(', '); if (!cliOptions.connector) { @@ -633,53 +627,63 @@ Target types and their options: ); } const connectorId = cliOptions.connector as ConnectorId; + + // Web search connector + if (connectorId === 'web-search') { + const excludeDomains = + typeof cliOptions.excludeDomains === 'string' + ? cliOptions.excludeDomains + .split(',') + .map((d: string) => d.trim()) + .filter((d: string) => d.length > 0) + : undefined; + const config: WebSearchTargetConfig = { + targetType: 'webSearch', + name: cliOptions.name!, + gateway: cliOptions.gateway!, + ...(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(',')})` : ''; + console.log(`Added web-search gateway target '${result.toolName}' on '${config.gateway}'${suffix}`); + } + return { ...telemetryAttrs, gateway_target_type: 'web-search' as const }; + } + + // KB connectors require --knowledge-base-id const kbRefs = cliOptions.knowledgeBaseId ?? []; if (kbRefs.length === 0) { throw new ValidationError(`--knowledge-base-id is required for --connector ${connectorId}.`); } - let config: ConnectorTargetConfig; - if (connectorId === 'bedrock-knowledge-bases') { - if (kbRefs.length > 1) { - throw new ValidationError( - '--knowledge-base-id may only be specified once for --connector bedrock-knowledge-bases. ' + - 'Use --connector bedrock-agentic-retrieve for fan-out across multiple KBs.' - ); - } - config = { - targetType: 'connector', - name: cliOptions.name!, - gateway: cliOptions.gateway!, - connectorId, - knowledgeBaseId: kbRefs[0]!, - ...(cliOptions.description && { description: cliOptions.description }), - }; - } else { - // bedrock-agentic-retrieve: fan-out via knowledgeBaseIds[]. - config = { - targetType: 'connector', - name: cliOptions.name!, - gateway: cliOptions.gateway!, - connectorId, - knowledgeBaseIds: kbRefs, - ...(cliOptions.description && { description: cliOptions.description }), - }; + if (kbRefs.length > 1) { + throw new ValidationError( + '--knowledge-base-id may only be specified once for --connector bedrock-knowledge-bases.' + ); } + + const resolvedName = + cliOptions.name ?? `kb-${cliOptions.gateway}-${kbRefs[0]}`.replace(/_/g, '-').slice(0, 128); + + const config: ConnectorTargetConfig = { + targetType: 'connector', + name: resolvedName, + gateway: cliOptions.gateway!, + connectorId, + knowledgeBaseId: kbRefs[0]!, + ...(cliOptions.description && { description: cliOptions.description }), + }; const result = await this.createConnectorGatewayTarget(config); const output = { success: true, toolName: result.toolName }; if (cliOptions.json) { console.log(JSON.stringify(output)); - } else if (config.connectorId === 'bedrock-agentic-retrieve') { - console.log( - `Added connector gateway target '${result.toolName}' on '${config.gateway}' → ${config.connectorId} (KBs ${kbRefs.join(', ')})` - ); } else { console.log( `Added connector gateway target '${result.toolName}' on '${config.gateway}' → ${config.connectorId} (KB ${kbRefs[0]})` ); - console.log( - `Also wired KB '${kbRefs[0]}' into gateway '${config.gateway}'-agentic (bedrock-agentic-retrieve fan-out)` - ); } return telemetryAttrs; } @@ -882,166 +886,6 @@ Target types and their options: process.exit(1); } }); - - // ────────────────────────────────────────────────────────────────── - // Top-level shortcuts: agentcore add web-search / remove web-search - // ────────────────────────────────────────────────────────────────── - addCmd - .command('web-search', { hidden: !isGatedFeaturesEnabled() }) - .description('Wire the Amazon Web Search managed connector to a gateway as a target.') - .option('--name ', 'Target name (default: web-search) [non-interactive]') - .option('--gateway ', 'Gateway to attach this target to [non-interactive]') - .option('--exclude-domains ', 'Comma-separated domains to exclude from results [non-interactive]') - .option('--json', 'Output as JSON [non-interactive]') - .action(async (cliOptions: { name?: string; gateway?: string; excludeDomains?: string; json?: boolean }) => { - if (!isGatedFeaturesEnabled()) { - console.error('Error: Web search target type is not yet available.'); - process.exit(1); - } - if (!findConfigRoot()) { - console.error('No agentcore project found. Run `agentcore create` first.'); - process.exit(1); - } - - const userPassedAnyFlag = - !!cliOptions.name || !!cliOptions.gateway || !!cliOptions.excludeDomains || !!cliOptions.json; - if (!userPassedAnyFlag) { - try { - requireTTY(); - const [{ render }, { default: React }, { AddWebSearchFlow }] = await Promise.all([ - import('ink'), - import('react'), - import('../tui/screens/web-search'), - ]); - const { clear, unmount } = render( - React.createElement(AddWebSearchFlow, { - isInteractive: false, - onBack: () => { - clear(); - unmount(); - process.exit(0); - }, - onExit: () => { - clear(); - unmount(); - process.exit(0); - }, - }) - ); - return; - } catch (error) { - console.error(getErrorMessage(error)); - process.exit(1); - } - } - - await runCliCommand('add.web-search', !!cliOptions.json, async () => { - // Default name `web-search` is convenient for the first invocation - // but produces a duplicate-target error on the second. Require an - // explicit --name when the default is already taken. - let resolvedName = cliOptions.name; - if (!resolvedName) { - const project = await this.readProjectSpec(); - const nameTaken = project.agentCoreGateways.some(g => (g.targets ?? []).some(t => t.name === 'web-search')); - if (nameTaken) { - throw new ValidationError( - 'A gateway target named "web-search" already exists. Pass --name to add another.' - ); - } - resolvedName = 'web-search'; - } - const forwardedOptions: CLIAddGatewayTargetOptions = { - name: resolvedName, - type: 'web-search', - gateway: cliOptions.gateway, - ...(cliOptions.excludeDomains && { excludeDomains: cliOptions.excludeDomains }), - }; - const validation = await validateAddGatewayTargetOptions(forwardedOptions); - if (!validation.valid) { - throw new ValidationError(validation.error!); - } - const excludeDomains = - typeof forwardedOptions.excludeDomains === 'string' - ? forwardedOptions.excludeDomains - .split(',') - .map((d: string) => d.trim()) - .filter((d: string) => d.length > 0) - : undefined; - const config: WebSearchTargetConfig = { - targetType: 'webSearch', - name: forwardedOptions.name!, - gateway: forwardedOptions.gateway!, - ...(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(',')})` : ''; - console.log(`Added web-search gateway target '${result.toolName}' on '${config.gateway}'${suffix}`); - } - return {}; - }); - }); - - removeCmd - .command('web-search', { hidden: !isGatedFeaturesEnabled() }) - .description('Remove an Amazon Web Search gateway target from the project') - .option('--name ', 'Name of the web-search target to remove [non-interactive]') - .option('-y, --yes', 'Skip confirmation prompt [non-interactive]') - .option('--json', 'Output as JSON [non-interactive]') - .action(async (cliOptions: { name?: string; yes?: boolean; json?: boolean }) => { - try { - if (!isGatedFeaturesEnabled()) { - console.error('Web search target type is not yet available.'); - process.exit(1); - } - if (!findConfigRoot()) { - console.error('No agentcore project found. Run `agentcore create` first.'); - process.exit(1); - } - - if (!cliOptions.name) { - throw new ValidationError('A --name is required for `agentcore remove web-search`.'); - } - const project = await this.readProjectSpec(); - const match = project.agentCoreGateways - .flatMap(g => (g.targets ?? []).map(t => ({ gateway: g.name, target: t }))) - .find(({ target }) => target.name === cliOptions.name); - if (!match) { - throw new ValidationError(`Gateway target "${cliOptions.name}" not found.`); - } - if (match.target.targetType !== 'webSearch') { - throw new ValidationError( - `Gateway target "${cliOptions.name}" is type "${match.target.targetType}", not webSearch. Use 'agentcore remove gateway-target --name ${cliOptions.name}' instead.` - ); - } - const result = await withCommandRunTelemetry('remove.web-search', {}, () => this.remove(cliOptions.name!)); - if (cliOptions.json) { - console.log( - JSON.stringify({ - success: result.success, - resourceType: this.kind, - resourceName: cliOptions.name, - message: result.success ? `Removed web-search gateway target '${cliOptions.name}'` : undefined, - error: !result.success ? result.error.message : undefined, - }) - ); - } else if (result.success) { - console.log(`Removed web-search gateway target '${cliOptions.name}'`); - } else { - throw result.error; - } - process.exit(result.success ? 0 : 1); - } catch (error) { - if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error: getErrorMessage(error) })); - } else { - console.error(`Error: ${getErrorMessage(error)}`); - } - process.exit(1); - } - }); } addScreen(): AddScreenComponent { @@ -1240,7 +1084,7 @@ Target types and their options: /** * Create a connector-typed gateway target backed by a managed AWS service - * (currently bedrock-knowledge-bases or bedrock-agentic-retrieve). + * (bedrock-knowledge-bases). * * Project-owned KB: config.knowledgeBaseId is a knowledgeBases[] entry name; * the L3 resolves it at synth time via application.knowledgeBases. @@ -1263,49 +1107,19 @@ Target types and their options: throw new Error(`Target "${config.name}" already exists in gateway "${gateway.name}".`); } - // For agentic-retrieve, refuse to silently shadow an existing one on the - // same gateway — the KB primitive would have created `${gateway}-agentic` - // already, and a user-driven low-level add should be an explicit choice. - if (config.connectorId === 'bedrock-agentic-retrieve') { - const existingAgentic = gateway.targets.find( - t => t.targetType === 'connector' && t.connectorId === 'bedrock-agentic-retrieve' - ); - if (existingAgentic) { - throw new Error( - `Gateway "${gateway.name}" already has a bedrock-agentic-retrieve target ("${existingAgentic.name}"). ` + - `Edit agentcore/agentcore.json directly to extend its knowledgeBaseIds[].` - ); - } - } + const configurations = translateConnector({ + connectorId: 'bedrock-knowledge-bases', + input: { knowledgeBaseId: config.knowledgeBaseId }, + }); - let target: AgentCoreGatewayTarget; - if (config.connectorId === 'bedrock-agentic-retrieve') { - target = { - name: config.name, - targetType: 'connector', - connectorId: config.connectorId, - knowledgeBaseIds: config.knowledgeBaseIds, - } as AgentCoreGatewayTarget; - } else { - target = { - name: config.name, - targetType: 'connector', - connectorId: config.connectorId, - knowledgeBaseId: config.knowledgeBaseId, - } as AgentCoreGatewayTarget; - } + const target: AgentCoreGatewayTarget = { + name: config.name, + targetType: 'connector', + connectorId: 'bedrock-knowledge-bases', + configurations, + } as AgentCoreGatewayTarget; gateway.targets.push(target); - - // Auto-upsert the shared agentic-retrieve target when wiring a single-KB - // Retrieve via this path, mirroring KnowledgeBasePrimitive.add({...gateway}). - // Without this, KBs added via `add gateway-target --type connector - // --connector bedrock-knowledge-bases` would be missing from the gateway's - // agentic-retrieve fan-out. - if (config.connectorId === 'bedrock-knowledge-bases') { - upsertAgenticRetrieveTarget(gateway, config.knowledgeBaseId); - } - await this.writeProjectSpec(project); return { toolName: config.name }; @@ -1402,10 +1216,16 @@ Target types and their options: throw new Error(`Target "${config.name}" already exists in gateway "${gateway.name}".`); } + const configurations = translateConnector({ + connectorId: 'web-search', + input: { excludeDomains: config.excludeDomains }, + }); + const target: AgentCoreGatewayTarget = { name: config.name, - targetType: 'webSearch', - ...(config.excludeDomains && config.excludeDomains.length > 0 ? { excludeDomains: config.excludeDomains } : {}), + targetType: 'connector', + connectorId: 'web-search', + configurations, } as AgentCoreGatewayTarget; gateway.targets.push(target); diff --git a/src/cli/primitives/KnowledgeBasePrimitive.ts b/src/cli/primitives/KnowledgeBasePrimitive.ts index e73cb8df4..309b3b7df 100644 --- a/src/cli/primitives/KnowledgeBasePrimitive.ts +++ b/src/cli/primitives/KnowledgeBasePrimitive.ts @@ -1,16 +1,9 @@ import { APP_DIR, ValidationError, findConfigRoot, serializeResult, toError } from '../../lib'; import type { Result } from '../../lib/result'; -import type { - AgentCoreGatewayTarget, - AgentCoreProjectSpec, - ConnectorFileDataSource, - DataSource, - KnowledgeBase, -} from '../../schema'; +import type { AgentCoreProjectSpec, ConnectorFileDataSource, DataSource, KnowledgeBase } from '../../schema'; import { CONNECTOR_ID, KnowledgeBaseSchema } from '../../schema'; import { getErrorMessage } from '../errors'; import { isGatedFeaturesEnabled } from '../feature-flags'; -import { upsertAgenticRetrieveTarget } from '../operations/knowledge-base/agentic-retrieve-upsert'; import { type DataSourceTypeFlag, flagToWireType, @@ -51,8 +44,6 @@ export interface AddKnowledgeBaseOptions { connectorConfig?: string[]; /** `--data-source-type` flag (s3 default, or web-crawler/confluence/...). */ dataSourceType?: DataSourceTypeFlag; - /** Gateway to wire the KB into via a connector target. Optional. */ - gateway?: string; json?: boolean; } @@ -62,8 +53,6 @@ export interface AddKnowledgeBaseSuccess extends Record { appended: boolean; /** New data source URIs added by this invocation (matches the order of --source flags). */ newDataSources: string[]; - /** Gateway the KB was wired to via a connector target, if any. */ - gatewayWired?: string; } export type RemovableKnowledgeBase = RemovableResource; @@ -196,15 +185,6 @@ export class KnowledgeBasePrimitive extends BasePrimitive g.name === options.gateway); - if (!gw) { - throw new Error( - `Gateway "${options.gateway}" not found in agentcore.json. Add it first with 'agentcore add gateway --name ${options.gateway}'.` - ); - } - } - // Phase 3 — all validation passed; now materialize (copy connector // configs into app//) and build the data sources. const newDataSources: DataSource[] = buildDataSources(); @@ -218,18 +198,9 @@ export class KnowledgeBasePrimitive extends BasePrimitive>, - gatewayName: string, - retrieveTargetName: string, - kbReference: string - ): void { - const gateway = project.agentCoreGateways.find(g => g.name === gatewayName); - if (!gateway) { - throw new Error(`Gateway "${gatewayName}" not found in agentcore.json.`); - } - this.upsertRetrieveTarget(gateway, retrieveTargetName, kbReference); - upsertAgenticRetrieveTarget(gateway, kbReference); - } - - /** - * Append a single-KB Retrieve target. Idempotent when the same target - * already exists pointing at the same KB; errors if a different target - * with the same name exists. - */ - private upsertRetrieveTarget( - gateway: AgentCoreProjectSpec['agentCoreGateways'][number], - targetName: string, - knowledgeBaseId: string - ): void { - const existingTarget = gateway.targets.find(t => t.name === targetName); - if (existingTarget) { - const sameKb = existingTarget.knowledgeBaseId === knowledgeBaseId; - const sameType = existingTarget.targetType === 'connector'; - const sameConnector = existingTarget.connectorId === CONNECTOR_ID.BEDROCK_KNOWLEDGE_BASES; - if (sameType && sameConnector && sameKb) { - return; - } - throw new Error(`Gateway "${gateway.name}" already has a target named "${targetName}". Pick a different --name.`); - } - const target: AgentCoreGatewayTarget = { - name: targetName, - targetType: 'connector', - connectorId: CONNECTOR_ID.BEDROCK_KNOWLEDGE_BASES, - knowledgeBaseId, - } as AgentCoreGatewayTarget; - gateway.targets.push(target); - } /** * Append data sources to an existing KB entry. Errors loudly on conflicting @@ -322,12 +246,6 @@ export class KnowledgeBasePrimitive extends BasePrimitive t.targetType === 'connector' && t.connectorId === CONNECTOR_ID.BEDROCK_AGENTIC_RETRIEVE - ); - if (agenticIdx === -1) continue; - const agentic = gw.targets[agenticIdx]!; - const ids = agentic.knowledgeBaseIds ?? []; - if (!ids.includes(kbReference)) continue; - const remaining = ids.filter(id => id !== kbReference); - if (remaining.length === 0) { - gw.targets.splice(agenticIdx, 1); - actions.push({ gatewayName: gw.name, targetName: agentic.name, removedTarget: true }); - } else { - agentic.knowledgeBaseIds = remaining; - actions.push({ gatewayName: gw.name, targetName: agentic.name, removedTarget: false }); + for (let tIdx = gw.targets.length - 1; tIdx >= 0; tIdx--) { + const target = gw.targets[tIdx]!; + if (target.targetType !== 'connector' || target.connectorId !== CONNECTOR_ID.BEDROCK_KNOWLEDGE_BASES) continue; + const configs = target.configurations ?? []; + + // Prune AgenticRetrieveStream: remove this KB from retrievers[] + const agenticCfgIdx = configs.findIndex(c => c.name === 'AgenticRetrieveStream'); + if (agenticCfgIdx !== -1) { + const agenticCfg = configs[agenticCfgIdx]!; + const pv = agenticCfg.parameterValues; + const retrievers = + (pv?.retrievers as { configuration: { knowledgeBase: { knowledgeBaseId: string } } }[] | undefined) ?? []; + const ids = retrievers.map(r => r.configuration.knowledgeBase.knowledgeBaseId); + if (ids.includes(kbReference)) { + const remaining = ids.filter(id => id !== kbReference); + if (remaining.length === 0) { + configs.splice(agenticCfgIdx, 1); + } else { + agenticCfg.parameterValues!.retrievers = remaining.map(id => ({ + configuration: { knowledgeBase: { knowledgeBaseId: id } }, + })); + } + } + } + + // Prune Retrieve: remove the config if it references this KB + const retrieveCfgIdx = configs.findIndex(c => { + return c.name === 'Retrieve' && c.parameterValues?.knowledgeBaseId === kbReference; + }); + if (retrieveCfgIdx !== -1) { + configs.splice(retrieveCfgIdx, 1); + } + + // If no configurations remain, remove the whole target + if (configs.length === 0) { + gw.targets.splice(tIdx, 1); + actions.push({ gatewayName: gw.name, targetName: target.name, removedTarget: true }); + } else if (agenticCfgIdx !== -1 || retrieveCfgIdx !== -1) { + actions.push({ gatewayName: gw.name, targetName: target.name, removedTarget: false }); + } } } return actions; @@ -522,7 +463,7 @@ export class KnowledgeBasePrimitive extends BasePrimitive', 'Knowledge base name (1-48 chars, starts with letter)') + .option('--name ', 'Knowledge base name (default: kb-quick-start-xxxxx)') .option('--description ', 'Optional description (used for tool discovery)') .option( '--source ', @@ -541,7 +482,6 @@ export class KnowledgeBasePrimitive extends BasePrimitive [...acc, val], [] as string[] ) - .option('--gateway ', 'Gateway to attach the KB to as a connector target.') .option('--json', 'Output as JSON [non-interactive]') .action( async (cliOptions: { @@ -550,7 +490,6 @@ export class KnowledgeBasePrimitive extends BasePrimitive { if (!isGatedFeaturesEnabled()) { @@ -572,7 +511,6 @@ export class KnowledgeBasePrimitive extends BasePrimitive 0 || (cliOptions.connectorConfig?.length ?? 0) > 0 || - !!cliOptions.gateway || !!cliOptions.json; if (!userPassedAnyFlag) { try { @@ -601,17 +539,23 @@ export class KnowledgeBasePrimitive extends BasePrimitive { - if (!cliOptions.name) { - throw new ValidationError('A --name is required for `agentcore add knowledge-base`.'); + let resolvedName = cliOptions.name; + if (!resolvedName) { + const project = await this.readProjectSpec(); + const existingNames = new Set(project.knowledgeBases.map(kb => kb.name)); + let candidate: string; + do { + candidate = `kb-quick-start-${Math.random().toString(36).slice(2, 7)}`; + } while (existingNames.has(candidate)); + resolvedName = candidate; } const result = await this.add({ - name: cliOptions.name, + name: resolvedName, description: cliOptions.description, source: cliOptions.source, dataSourceType: cliOptions.dataSourceType as DataSourceTypeFlag | undefined, connectorConfig: cliOptions.connectorConfig, - gateway: cliOptions.gateway, json: cliOptions.json, }); @@ -625,24 +569,17 @@ export class KnowledgeBasePrimitive extends BasePrimitive { afterEach(() => vi.restoreAllMocks()); - it('writes a single-KB Retrieve target for bedrock-knowledge-bases', async () => { + it('writes a connector target with configurations for bedrock-knowledge-bases', async () => { const { primitive, getProject } = makePrimitive(emptyProject()); const result = await primitive.createConnectorGatewayTarget({ targetType: 'connector', @@ -244,11 +244,11 @@ describe('GatewayTargetPrimitive — createConnectorGatewayTarget', () => { const targets = getProject().agentCoreGateways[0]?.targets ?? []; const retrieve = targets.find(t => t.connectorId === 'bedrock-knowledge-bases'); expect(retrieve?.connectorId).toBe('bedrock-knowledge-bases'); - expect(retrieve?.knowledgeBaseId).toBe('ABCDEFGHIJ'); - expect(retrieve?.knowledgeBaseIds).toBeUndefined(); + const retrieveConfig = (retrieve?.configurations ?? []).find(c => c.name === 'Retrieve'); + expect((retrieveConfig?.parameterValues as any)?.knowledgeBaseId).toBe('ABCDEFGHIJ'); }); - it('bedrock-knowledge-bases create also upserts a shared agentic-retrieve target', async () => { + it('bedrock-knowledge-bases creates target with both Retrieve and AgenticRetrieveStream configurations', async () => { const { primitive, getProject } = makePrimitive(emptyProject()); await primitive.createConnectorGatewayTarget({ targetType: 'connector', @@ -258,16 +258,18 @@ describe('GatewayTargetPrimitive — createConnectorGatewayTarget', () => { knowledgeBaseId: 'ABCDEFGHIJ', }); const targets = getProject().agentCoreGateways[0]?.targets ?? []; - expect(targets).toHaveLength(2); - const retrieve = targets.find(t => t.connectorId === 'bedrock-knowledge-bases'); - expect(retrieve?.name).toBe('product-docs'); - expect(retrieve?.knowledgeBaseId).toBe('ABCDEFGHIJ'); - const agentic = targets.find(t => t.connectorId === 'bedrock-agentic-retrieve'); - expect(agentic?.name).toBe('main-gw-agentic'); - expect(agentic?.knowledgeBaseIds).toEqual(['ABCDEFGHIJ']); + expect(targets).toHaveLength(1); + const target = targets.find(t => t.connectorId === 'bedrock-knowledge-bases'); + expect(target?.name).toBe('product-docs'); + const retrieveConfig = (target?.configurations ?? []).find(c => c.name === 'Retrieve'); + expect((retrieveConfig?.parameterValues as any)?.knowledgeBaseId).toBe('ABCDEFGHIJ'); + const agenticConfig = (target?.configurations ?? []).find(c => c.name === 'AgenticRetrieveStream'); + expect(agenticConfig).toBeDefined(); + const retrievers = (agenticConfig?.parameterValues as any)?.retrievers as any[]; + expect(retrievers?.map((r: any) => r.configuration.knowledgeBase.knowledgeBaseId)).toEqual(['ABCDEFGHIJ']); }); - it('two bedrock-knowledge-bases creates share a single agentic target with both KBs', async () => { + it('two bedrock-knowledge-bases creates produce two separate targets each with their own configurations', async () => { const { primitive, getProject } = makePrimitive(emptyProject()); await primitive.createConnectorGatewayTarget({ targetType: 'connector', @@ -284,24 +286,21 @@ describe('GatewayTargetPrimitive — createConnectorGatewayTarget', () => { knowledgeBaseId: 'KLMNOPQRST', }); const targets = getProject().agentCoreGateways[0]?.targets ?? []; - // Two Retrieve targets + one shared agentic target. - expect(targets).toHaveLength(3); - const agentics = targets.filter(t => t.connectorId === 'bedrock-agentic-retrieve'); - expect(agentics).toHaveLength(1); - expect(agentics[0]?.knowledgeBaseIds).toEqual(['ABCDEFGHIJ', 'KLMNOPQRST']); + expect(targets).toHaveLength(2); + const targetA = targets.find(t => t.name === 'docs-a'); + const targetB = targets.find(t => t.name === 'docs-b'); + const agenticA = (targetA?.configurations ?? []).find(c => c.name === 'AgenticRetrieveStream'); + const agenticB = (targetB?.configurations ?? []).find(c => c.name === 'AgenticRetrieveStream'); + expect((agenticA?.parameterValues as any)?.retrievers?.[0]?.configuration?.knowledgeBase?.knowledgeBaseId).toBe( + 'ABCDEFGHIJ' + ); + expect((agenticB?.parameterValues as any)?.retrievers?.[0]?.configuration?.knowledgeBase?.knowledgeBaseId).toBe( + 'KLMNOPQRST' + ); }); - it('appends to an existing agentic target created earlier (e.g. via the KB primitive)', async () => { - const initial = emptyProject(); - initial.agentCoreGateways[0]!.targets = [ - { - name: 'main-gw-agentic', - targetType: 'connector', - connectorId: 'bedrock-agentic-retrieve', - knowledgeBaseIds: ['existing-kb'], - } as unknown as AgentCoreProjectSpec['agentCoreGateways'][0]['targets'][0], - ]; - const { primitive, getProject } = makePrimitive(initial); + it('rejects a duplicate target name on the same gateway', async () => { + const { primitive } = makePrimitive(emptyProject()); await primitive.createConnectorGatewayTarget({ targetType: 'connector', name: 'product-docs', @@ -309,55 +308,22 @@ describe('GatewayTargetPrimitive — createConnectorGatewayTarget', () => { connectorId: 'bedrock-knowledge-bases', knowledgeBaseId: 'ABCDEFGHIJ', }); - const targets = getProject().agentCoreGateways[0]?.targets ?? []; - const agentics = targets.filter(t => t.connectorId === 'bedrock-agentic-retrieve'); - expect(agentics).toHaveLength(1); - expect(agentics[0]?.knowledgeBaseIds).toEqual(['existing-kb', 'ABCDEFGHIJ']); - expect(targets.find(t => t.connectorId === 'bedrock-knowledge-bases')?.name).toBe('product-docs'); - }); - - it('writes a fan-out agentic-retrieve target with knowledgeBaseIds[]', async () => { - const { primitive, getProject } = makePrimitive(emptyProject()); - await primitive.createConnectorGatewayTarget({ - targetType: 'connector', - name: 'agentic', - gateway: 'main-gw', - connectorId: 'bedrock-agentic-retrieve', - knowledgeBaseIds: ['ABCDEFGHIJ', 'KLMNOPQRST'], - }); - const target = getProject().agentCoreGateways[0]?.targets[0]; - expect(target?.connectorId).toBe('bedrock-agentic-retrieve'); - expect(target?.knowledgeBaseIds).toEqual(['ABCDEFGHIJ', 'KLMNOPQRST']); - expect(target?.knowledgeBaseId).toBeUndefined(); - }); - - it('rejects a second agentic-retrieve target on the same gateway', async () => { - const initial = emptyProject(); - initial.agentCoreGateways[0]!.targets = [ - { - name: 'main-gw-agentic', - targetType: 'connector', - connectorId: 'bedrock-agentic-retrieve', - knowledgeBaseIds: ['existing'], - } as unknown as AgentCoreProjectSpec['agentCoreGateways'][0]['targets'][0], - ]; - const { primitive } = makePrimitive(initial); await expect( primitive.createConnectorGatewayTarget({ targetType: 'connector', - name: 'another-agentic', + name: 'product-docs', gateway: 'main-gw', - connectorId: 'bedrock-agentic-retrieve', - knowledgeBaseIds: ['ABCDEFGHIJ'], + connectorId: 'bedrock-knowledge-bases', + knowledgeBaseId: 'KLMNOPQRST', }) - ).rejects.toThrow(/already has a bedrock-agentic-retrieve target/); + ).rejects.toThrow(/already exists/); }); }); describe('GatewayTargetPrimitive — createWebSearchGatewayTarget', () => { afterEach(() => vi.restoreAllMocks()); - it('writes a webSearch target with no excludeDomains when omitted', async () => { + it('writes a connector target with connectorId web-search when excludeDomains omitted', async () => { const { primitive, getProject } = makePrimitive(emptyProject()); const result = await primitive.createWebSearchGatewayTarget({ targetType: 'webSearch', @@ -366,12 +332,15 @@ describe('GatewayTargetPrimitive — createWebSearchGatewayTarget', () => { }); expect(result.toolName).toBe('web-search'); const target = getProject().agentCoreGateways[0]?.targets[0]; - expect(target?.targetType).toBe('webSearch'); + expect(target?.targetType).toBe('connector'); + expect(target?.connectorId).toBe('web-search'); expect(target?.name).toBe('web-search'); - expect(target?.excludeDomains).toBeUndefined(); + const wsConfig = (target?.configurations ?? []).find(c => c.name === 'WebSearch'); + expect(wsConfig).toBeDefined(); + expect(wsConfig?.parameterValues).toEqual({}); }); - it('persists excludeDomains when provided', async () => { + it('persists excludeDomains in configurations when provided', async () => { const { primitive, getProject } = makePrimitive(emptyProject()); await primitive.createWebSearchGatewayTarget({ targetType: 'webSearch', @@ -380,7 +349,11 @@ describe('GatewayTargetPrimitive — createWebSearchGatewayTarget', () => { excludeDomains: ['internal.example.com', 'staging.example.com'], }); const target = getProject().agentCoreGateways[0]?.targets[0]; - expect(target?.excludeDomains).toEqual(['internal.example.com', 'staging.example.com']); + const wsConfig = (target?.configurations ?? []).find(c => c.name === 'WebSearch'); + expect((wsConfig?.parameterValues as any)?.domainFilter?.exclude).toEqual([ + 'internal.example.com', + 'staging.example.com', + ]); }); it('rejects a duplicate target name on the same gateway', async () => { diff --git a/src/cli/primitives/__tests__/KnowledgeBasePrimitive.test.ts b/src/cli/primitives/__tests__/KnowledgeBasePrimitive.test.ts index d984087a7..2f0c7ff60 100644 --- a/src/cli/primitives/__tests__/KnowledgeBasePrimitive.test.ts +++ b/src/cli/primitives/__tests__/KnowledgeBasePrimitive.test.ts @@ -140,23 +140,6 @@ describe('add knowledge-base — non-S3 connectors', () => { expect(existsSync(join(projectRoot, 'app'))).toBe(false); }); - it('does not copy any file when the gateway is missing', async () => { - const { primitive, projectRoot } = makePrimitiveWithProjectDir(emptyProject()); - tmpDirs.push(projectRoot); - const cfg = writeConfig(projectRoot, 'web.json', { - type: 'WEB', - connectionConfiguration: { authType: 'NO_AUTH' }, - }); - const r = await primitive.add({ - name: 'kb', - dataSourceType: 'web-crawler', - connectorConfig: [cfg], - gateway: 'missing-gw', - }); - expect(r.success).toBe(false); - expect(existsSync(join(projectRoot, 'app'))).toBe(false); - }); - it('rejects two connector configs with the same basename and copies nothing', async () => { const { primitive, getProject, projectRoot } = makePrimitiveWithProjectDir(emptyProject()); tmpDirs.push(projectRoot); @@ -327,97 +310,6 @@ describe('KnowledgeBasePrimitive — add (new KB)', () => { expect(result.error.message).toMatch(/Invalid S3 URI/i); }); - it('errors when --gateway references a gateway not in agentCoreGateways[]', async () => { - const { primitive } = makePrimitive(emptyProject()); - const result = await primitive.add({ - name: 'docs', - source: ['s3://my-bucket/a/'], - gateway: 'missing-gw', - }); - expect(result.success).toBe(false); - if (result.success) return; - expect(result.error.message).toMatch(/Gateway "missing-gw" not found/i); - }); - - it('with --gateway: emits a Retrieve target AND a gateway-scoped agentic-retrieve target', async () => { - const initial = emptyProject(); - initial.agentCoreGateways.push({ - name: 'main-gw', - targets: [], - authorizerType: 'NONE', - enableSemanticSearch: true, - exceptionLevel: 'NONE', - } as unknown as AgentCoreProjectSpec['agentCoreGateways'][0]); - const { primitive, getProject } = makePrimitive(initial); - - const result = await primitive.add({ - name: 'docs', - source: ['s3://my-bucket/a/'], - gateway: 'main-gw', - }); - - expect(result.success).toBe(true); - if (!result.success) return; - expect(result.gatewayWired).toBe('main-gw'); - - const project = getProject(); - expect(project.knowledgeBases[0]?.gateway).toBe('main-gw'); - const targets = project.agentCoreGateways[0]?.targets ?? []; - expect(targets).toHaveLength(2); - - const retrieve = targets.find(t => t.name === 'docs'); - expect(retrieve?.targetType).toBe('connector'); - expect(retrieve?.connectorId).toBe('bedrock-knowledge-bases'); - // The connector target stores the KB *name*; the L3 looks it up at synth. - expect(retrieve?.knowledgeBaseId).toBe('docs'); - - const agentic = targets.find(t => t.connectorId === 'bedrock-agentic-retrieve'); - expect(agentic?.name).toBe('main-gw-agentic'); - expect(agentic?.knowledgeBaseIds).toEqual(['docs']); - }); - - it('second KB on the same gateway appends to the existing agentic-retrieve target', async () => { - const initial = emptyProject(); - initial.agentCoreGateways.push({ - name: 'main-gw', - targets: [], - authorizerType: 'NONE', - enableSemanticSearch: true, - exceptionLevel: 'NONE', - } as unknown as AgentCoreProjectSpec['agentCoreGateways'][0]); - const { primitive, getProject } = makePrimitive(initial); - - await primitive.add({ name: 'docs', source: ['s3://my-bucket/a/'], gateway: 'main-gw' }); - await primitive.add({ name: 'hr', source: ['s3://my-bucket/b/'], gateway: 'main-gw' }); - - const targets = getProject().agentCoreGateways[0]?.targets ?? []; - // Two Retrieve targets + one agentic target. - expect(targets).toHaveLength(3); - expect(targets.filter(t => t.connectorId === 'bedrock-knowledge-bases').map(t => t.name)).toEqual(['docs', 'hr']); - const agentic = targets.find(t => t.connectorId === 'bedrock-agentic-retrieve'); - expect(agentic?.name).toBe('main-gw-agentic'); - expect(agentic?.knowledgeBaseIds).toEqual(['docs', 'hr']); - }); - - it('idempotent re-add: same KB twice does not duplicate it in the agentic-retrieve target', async () => { - const initial = emptyProject(); - initial.agentCoreGateways.push({ - name: 'main-gw', - targets: [], - authorizerType: 'NONE', - enableSemanticSearch: true, - exceptionLevel: 'NONE', - } as unknown as AgentCoreProjectSpec['agentCoreGateways'][0]); - const { primitive, getProject } = makePrimitive(initial); - - await primitive.add({ name: 'docs', source: ['s3://my-bucket/a/'], gateway: 'main-gw' }); - // Append a new data source on the same KB; --gateway is the same. - await primitive.add({ name: 'docs', source: ['s3://my-bucket/c/'], gateway: 'main-gw' }); - - const agentic = getProject().agentCoreGateways[0]?.targets.find(t => t.connectorId === 'bedrock-agentic-retrieve'); - expect(agentic?.knowledgeBaseIds).toEqual(['docs']); - }); - it('rejects duplicate --source URIs within the same invocation', async () => { const { primitive } = makePrimitive(emptyProject()); const result = await primitive.add({ @@ -610,13 +502,31 @@ describe('KnowledgeBasePrimitive — remove', () => { initial.agentCoreGateways.push({ name: 'main-gw', targets: [ - { name: 'docs', targetType: 'connector', connectorId: 'bedrock-knowledge-bases', knowledgeBaseId: 'docs' }, - { name: 'hr', targetType: 'connector', connectorId: 'bedrock-knowledge-bases', knowledgeBaseId: 'hr' }, { - name: 'main-gw-agentic', + name: 'docs', targetType: 'connector', - connectorId: 'bedrock-agentic-retrieve', - knowledgeBaseIds: ['docs', 'hr'], + connectorId: 'bedrock-knowledge-bases', + configurations: [ + { name: 'Retrieve', parameterValues: { knowledgeBaseId: 'docs' }, parameterOverrides: [] }, + { + name: 'AgenticRetrieveStream', + parameterValues: { retrievers: [{ configuration: { knowledgeBase: { knowledgeBaseId: 'docs' } } }] }, + parameterOverrides: [], + }, + ], + }, + { + name: 'hr', + targetType: 'connector', + connectorId: 'bedrock-knowledge-bases', + configurations: [ + { name: 'Retrieve', parameterValues: { knowledgeBaseId: 'hr' }, parameterOverrides: [] }, + { + name: 'AgenticRetrieveStream', + parameterValues: { retrievers: [{ configuration: { knowledgeBase: { knowledgeBaseId: 'hr' } } }] }, + parameterOverrides: [], + }, + ], }, ], authorizerType: 'NONE', @@ -629,14 +539,16 @@ describe('KnowledgeBasePrimitive — remove', () => { expect(result.success).toBe(true); const targets = getProject().agentCoreGateways[0]?.targets ?? []; - // Per-KB Retrieve target gone; agentic target stays with hr only. + // Per-KB target gone; hr target remains. expect(targets.find(t => t.name === 'docs')).toBeUndefined(); - const agentic = targets.find(t => t.connectorId === 'bedrock-agentic-retrieve'); - expect(agentic).toBeDefined(); - expect(agentic?.knowledgeBaseIds).toEqual(['hr']); + const hr = targets.find(t => t.name === 'hr'); + expect(hr).toBeDefined(); + const agenticCfg = (hr?.configurations ?? []).find(c => c.name === 'AgenticRetrieveStream'); + const retrievers = (agenticCfg?.parameterValues as any)?.retrievers as any[]; + expect(retrievers?.map((r: any) => r.configuration.knowledgeBase.knowledgeBaseId)).toEqual(['hr']); }); - it('removes the agentic-retrieve target entirely when the removed KB was its only entry', async () => { + it('removes the connector target entirely when the removed KB was the only one', async () => { const initial = emptyProject(); initial.knowledgeBases = [ { @@ -649,12 +561,18 @@ describe('KnowledgeBasePrimitive — remove', () => { initial.agentCoreGateways.push({ name: 'main-gw', targets: [ - { name: 'docs', targetType: 'connector', connectorId: 'bedrock-knowledge-bases', knowledgeBaseId: 'docs' }, { - name: 'main-gw-agentic', + name: 'docs', targetType: 'connector', - connectorId: 'bedrock-agentic-retrieve', - knowledgeBaseIds: ['docs'], + connectorId: 'bedrock-knowledge-bases', + configurations: [ + { name: 'Retrieve', parameterValues: { knowledgeBaseId: 'docs' }, parameterOverrides: [] }, + { + name: 'AgenticRetrieveStream', + parameterValues: { retrievers: [{ configuration: { knowledgeBase: { knowledgeBaseId: 'docs' } } }] }, + parameterOverrides: [], + }, + ], }, ], authorizerType: 'NONE', @@ -670,7 +588,7 @@ describe('KnowledgeBasePrimitive — remove', () => { expect(targets).toHaveLength(0); }); - it('previewRemove summarizes the agentic-retrieve prune', async () => { + it('previewRemove summarizes removal of the KB target', async () => { const initial = emptyProject(); initial.knowledgeBases = [ { @@ -689,13 +607,31 @@ describe('KnowledgeBasePrimitive — remove', () => { initial.agentCoreGateways.push({ name: 'main-gw', targets: [ - { name: 'docs', targetType: 'connector', connectorId: 'bedrock-knowledge-bases', knowledgeBaseId: 'docs' }, - { name: 'hr', targetType: 'connector', connectorId: 'bedrock-knowledge-bases', knowledgeBaseId: 'hr' }, { - name: 'main-gw-agentic', + name: 'docs', + targetType: 'connector', + connectorId: 'bedrock-knowledge-bases', + configurations: [ + { name: 'Retrieve', parameterValues: { knowledgeBaseId: 'docs' }, parameterOverrides: [] }, + { + name: 'AgenticRetrieveStream', + parameterValues: { retrievers: [{ configuration: { knowledgeBase: { knowledgeBaseId: 'docs' } } }] }, + parameterOverrides: [], + }, + ], + }, + { + name: 'hr', targetType: 'connector', - connectorId: 'bedrock-agentic-retrieve', - knowledgeBaseIds: ['docs', 'hr'], + connectorId: 'bedrock-knowledge-bases', + configurations: [ + { name: 'Retrieve', parameterValues: { knowledgeBaseId: 'hr' }, parameterOverrides: [] }, + { + name: 'AgenticRetrieveStream', + parameterValues: { retrievers: [{ configuration: { knowledgeBase: { knowledgeBaseId: 'hr' } } }] }, + parameterOverrides: [], + }, + ], }, ], authorizerType: 'NONE', @@ -706,10 +642,10 @@ describe('KnowledgeBasePrimitive — remove', () => { const preview = await primitive.previewRemove('docs'); const lines = preview.summary.join('\n'); - expect(lines).toMatch(/main-gw.*agentic-retrieve target 'main-gw-agentic' will lose KB 'docs'/); + expect(lines).toMatch(/Gateway target: 'docs' on 'main-gw' will be removed/); }); - it('previewRemove notes when the agentic-retrieve target itself will be removed', async () => { + it('previewRemove notes when the last KB target will be removed', async () => { const initial = emptyProject(); initial.knowledgeBases = [ { @@ -722,12 +658,18 @@ describe('KnowledgeBasePrimitive — remove', () => { initial.agentCoreGateways.push({ name: 'main-gw', targets: [ - { name: 'docs', targetType: 'connector', connectorId: 'bedrock-knowledge-bases', knowledgeBaseId: 'docs' }, { - name: 'main-gw-agentic', + name: 'docs', targetType: 'connector', - connectorId: 'bedrock-agentic-retrieve', - knowledgeBaseIds: ['docs'], + connectorId: 'bedrock-knowledge-bases', + configurations: [ + { name: 'Retrieve', parameterValues: { knowledgeBaseId: 'docs' }, parameterOverrides: [] }, + { + name: 'AgenticRetrieveStream', + parameterValues: { retrievers: [{ configuration: { knowledgeBase: { knowledgeBaseId: 'docs' } } }] }, + parameterOverrides: [], + }, + ], }, ], authorizerType: 'NONE', @@ -738,6 +680,6 @@ describe('KnowledgeBasePrimitive — remove', () => { const preview = await primitive.previewRemove('docs'); const lines = preview.summary.join('\n'); - expect(lines).toMatch(/main-gw.*agentic-retrieve target 'main-gw-agentic' will be removed \(was the last KB\)/); + expect(lines).toMatch(/Gateway target: 'docs' on 'main-gw' will be removed/); }); }); diff --git a/src/cli/telemetry/schemas/command-run.ts b/src/cli/telemetry/schemas/command-run.ts index 3df3888c5..4c35c3a0f 100644 --- a/src/cli/telemetry/schemas/command-run.ts +++ b/src/cli/telemetry/schemas/command-run.ts @@ -97,7 +97,6 @@ const AddPolicyEngineAttrs = safeSchema({ attach_gateway_count: Count, attach_mo const AddKnowledgeBaseAttrs = safeSchema({ data_source_count: Count, has_description: z.boolean(), - has_gateway: z.boolean(), is_append: z.boolean(), }); @@ -226,7 +225,6 @@ export const COMMAND_SCHEMAS = { 'add.online-insights': AddOnlineInsightsAttrs, 'add.gateway': AddGatewayAttrs, 'add.gateway-target': AddGatewayTargetAttrs, - 'add.web-search': NoAttrs, 'add.policy-engine': AddPolicyEngineAttrs, 'add.policy': AddPolicyAttrs, 'add.runtime-endpoint': NoAttrs, @@ -286,7 +284,6 @@ export const COMMAND_SCHEMAS = { 'remove.online-insights': NoAttrs, 'remove.gateway': NoAttrs, 'remove.gateway-target': NoAttrs, - 'remove.web-search': NoAttrs, 'remove.policy-engine': NoAttrs, 'remove.policy': NoAttrs, 'remove.runtime-endpoint': NoAttrs, diff --git a/src/cli/tui/screens/add/AddFlow.tsx b/src/cli/tui/screens/add/AddFlow.tsx index 8f25be054..99ad8cb92 100644 --- a/src/cli/tui/screens/add/AddFlow.tsx +++ b/src/cli/tui/screens/add/AddFlow.tsx @@ -20,7 +20,6 @@ import { AddOnlineInsightsFlow } from '../online-insights'; import { AddPaymentFlow } from '../payment'; import { AddPolicyFlow } from '../policy'; import { AddRuntimeEndpointFlow } from '../runtime-endpoint'; -import { AddWebSearchFlow } from '../web-search'; import type { AddResourceType } from './AddScreen'; import { AddScreen } from './AddScreen'; import { AddSuccessScreen } from './AddSuccessScreen'; @@ -36,7 +35,6 @@ type FlowState = | { name: 'tool-wizard' } | { name: 'memory-wizard' } | { name: 'knowledge-base-wizard' } - | { name: 'web-search-wizard' } | { name: 'identity-wizard' } | { name: 'evaluator-wizard' } | { name: 'online-eval-wizard' } @@ -192,8 +190,6 @@ function getInitialFlowState(resource?: AddResourceType): FlowState { return { name: 'memory-wizard' }; case 'knowledge-base': return { name: 'knowledge-base-wizard' }; - case 'web-search': - return { name: 'web-search-wizard' }; case 'credential': return { name: 'identity-wizard' }; case 'evaluator': @@ -255,9 +251,6 @@ export function AddFlow(props: AddFlowProps) { case 'knowledge-base': setFlow({ name: 'knowledge-base-wizard' }); break; - case 'web-search': - setFlow({ name: 'web-search-wizard' }); - break; case 'credential': setFlow({ name: 'identity-wizard' }); break; @@ -501,19 +494,6 @@ export function AddFlow(props: AddFlowProps) { ); } - // Web search wizard - if (flow.name === 'web-search-wizard') { - return ( - setFlow({ name: 'select' })} - onExit={props.onExit} - onDev={props.onDev} - onDeploy={props.onDeploy} - /> - ); - } - // Identity wizard - now uses AddIdentityFlow with mode selection if (flow.name === 'identity-wizard') { return ( diff --git a/src/cli/tui/screens/add/AddScreen.tsx b/src/cli/tui/screens/add/AddScreen.tsx index 5da5bda5b..3676e5689 100644 --- a/src/cli/tui/screens/add/AddScreen.tsx +++ b/src/cli/tui/screens/add/AddScreen.tsx @@ -7,7 +7,6 @@ export type AddResourceType = | 'agent' | 'memory' | 'knowledge-base' - | 'web-search' | 'credential' | 'evaluator' | 'online-eval' @@ -25,7 +24,6 @@ const BASE_ADD_RESOURCES: { id: AddResourceType; title: string; description: str { id: 'agent', title: 'Agent', description: 'Deploy an HTTP, MCP, A2A, or AG-UI agent' }, { id: 'memory', title: 'Memory', description: 'Persistent context storage' }, { id: 'knowledge-base', title: 'Knowledge Base', description: 'Create a managed knowledge base for retrieval' }, - { id: 'web-search', title: 'Web Search', description: 'Wire the Amazon Web Search managed connector to a gateway' }, { id: 'credential', title: 'Credential', description: 'API key credential providers' }, { id: 'evaluator', title: 'Evaluator', description: 'Custom LLM-as-a-Judge evaluator' }, { id: 'online-eval', title: 'Online Eval Config', description: 'Continuous evaluation pipeline' }, @@ -50,7 +48,7 @@ const ADD_RESOURCES: { id: AddResourceType; title: string; description: string } ]; const ADD_RESOURCE_ITEMS: SelectableItem[] = ADD_RESOURCES.map(r => { - const gated = (r.id === 'knowledge-base' || r.id === 'web-search') && !isGatedFeaturesEnabled(); + const gated = r.id === 'knowledge-base' && !isGatedFeaturesEnabled(); return { ...r, disabled: gated, diff --git a/src/cli/tui/screens/add/__tests__/AddScreen.test.tsx b/src/cli/tui/screens/add/__tests__/AddScreen.test.tsx index 7ca2a3cb7..82ae13de0 100644 --- a/src/cli/tui/screens/add/__tests__/AddScreen.test.tsx +++ b/src/cli/tui/screens/add/__tests__/AddScreen.test.tsx @@ -32,15 +32,4 @@ describe('AddScreen', () => { expect(lastFrame()).toContain('Payment Manager'); expect(lastFrame()).toContain('Payment Connector'); }); - - it('Web Search option shows Coming soon when ENABLE_GATED_FEATURES is unset', () => { - delete process.env.ENABLE_GATED_FEATURES; - const onSelect = vi.fn(); - const onExit = vi.fn(); - - const { lastFrame } = render(); - - expect(lastFrame()).toContain('Web Search'); - expect(lastFrame()).toContain('Coming soon'); - }); }); diff --git a/src/cli/tui/screens/knowledge-base/AddKnowledgeBaseFlow.tsx b/src/cli/tui/screens/knowledge-base/AddKnowledgeBaseFlow.tsx index 8a462a087..947b2afd3 100644 --- a/src/cli/tui/screens/knowledge-base/AddKnowledgeBaseFlow.tsx +++ b/src/cli/tui/screens/knowledge-base/AddKnowledgeBaseFlow.tsx @@ -1,6 +1,5 @@ -import { gatewayPrimitive, knowledgeBasePrimitive } from '../../../primitives/registry'; +import { knowledgeBasePrimitive } from '../../../primitives/registry'; import { ErrorPrompt } from '../../components'; -import { useExistingGateways } from '../../hooks/useCreateMcp'; import { AddSuccessScreen } from '../add/AddSuccessScreen'; import { AddKnowledgeBaseScreen } from './AddKnowledgeBaseScreen'; import { groupDataSources } from './groupDataSources'; @@ -10,7 +9,7 @@ import React, { useCallback, useEffect, useState } from 'react'; type FlowState = | { name: 'create-wizard' } - | { name: 'create-success'; knowledgeBaseName: string; sources: string[]; gatewayWired?: string } + | { name: 'create-success'; knowledgeBaseName: string; sources: string[] } | { name: 'error'; message: string }; interface AddKnowledgeBaseFlowProps { @@ -30,7 +29,6 @@ export function AddKnowledgeBaseFlow({ }: AddKnowledgeBaseFlowProps) { const [flow, setFlow] = useState({ name: 'create-wizard' }); const [existingNames, setExistingNames] = useState([]); - const { gateways: existingGateways } = useExistingGateways(); // Load existing KB names for duplicate detection. useEffect(() => { @@ -48,12 +46,6 @@ export function AddKnowledgeBaseFlow({ const handleComplete = useCallback((config: AddKnowledgeBaseConfig) => { void (async () => { - // Materialize any inline-JSON connector configs to disk first. The - // wizard tags those entries with INLINE_JSON_PREFIX; we strip the - // prefix, write the JSON to app//.json, and replace - // the captured value with the resulting path so the primitive sees a - // normal connector-config path. Failures here surface to the user as a - // wizard error before any primitive call. let materializedSources: CapturedDataSource[]; try { materializedSources = await Promise.all( @@ -76,59 +68,13 @@ export function AddKnowledgeBaseFlow({ return; } - // Group captured sources by data-source-type, then dispatch one - // primitive.add() per group sequentially: the first call creates the - // KB, subsequent calls hit appendToExisting() and add their sources to - // the same KB. The primitive's gateway-equality guard accepts the same - // gateway value on every append; description is sent only on the first - // call so the no-update guard can't trip. const groups = groupDataSources(materializedSources); if (groups.length === 0) { setFlow({ name: 'error', message: 'No data sources captured.' }); return; } - // If the user chose "Create a new gateway and attach", create the - // gateway BEFORE the KB add. Use sensible defaults — authorizer NONE, - // semantic search on — so the inline-create stays a single step. The - // user can edit the gateway later via `agentcore add gateway` flags or - // the schema directly. Mutually exclusive with `config.gateway`. - // - // Track whether we created the gateway in *this* flow so we can roll it - // back if a downstream KB add fails. Without this, a failure mid-flow - // (duplicate source, gateway-equality mismatch, etc.) leaves the new - // gateway persisted in agentcore.json with no KB attached — the user - // sees an error toast but their config has drifted. - let gatewayToWire: string | undefined = config.gateway; - let createdGatewayInThisFlow: string | undefined; - if (config.newGatewayName) { - const gwResult = await gatewayPrimitive.add({ - name: config.newGatewayName, - authorizerType: 'NONE', - enableSemanticSearch: true, - }); - if (!gwResult.success) { - setFlow({ - name: 'error', - message: `Failed to create gateway "${config.newGatewayName}": ${gwResult.error.message}`, - }); - return; - } - gatewayToWire = gwResult.gatewayName; - createdGatewayInThisFlow = gwResult.gatewayName; - } - - const rollbackGatewayIfCreated = async (reason: string): Promise => { - if (!createdGatewayInThisFlow) return reason; - const removeResult = await gatewayPrimitive.remove(createdGatewayInThisFlow); - if (removeResult.success) { - return `${reason} (rolled back the gateway "${createdGatewayInThisFlow}" that was just created.)`; - } - return `${reason} (note: gateway "${createdGatewayInThisFlow}" was created but rollback failed: ${removeResult.error?.message ?? 'unknown error'}. Run \`agentcore remove gateway --name ${createdGatewayInThisFlow}\` to clean up.)`; - }; - const totalSources: string[] = []; - let gatewayWired: string | undefined; for (let i = 0; i < groups.length; i++) { const group = groups[i]!; @@ -139,49 +85,35 @@ export function AddKnowledgeBaseFlow({ ...(isFirst && config.description ? { description: config.description } : {}), dataSourceType: group.dataSourceType, ...(isS3 ? { source: group.values } : { connectorConfig: group.values }), - gateway: gatewayToWire, }); if (!result.success) { - const message = await rollbackGatewayIfCreated( - `Failed on ${group.dataSourceType} group: ${result.error.message}` - ); - setFlow({ name: 'error', message }); + setFlow({ name: 'error', message: `Failed on ${group.dataSourceType} group: ${result.error.message}` }); return; } totalSources.push(...result.newDataSources); - if (result.gatewayWired) { - gatewayWired = result.gatewayWired; - } } setFlow({ name: 'create-success', knowledgeBaseName: config.name, sources: totalSources, - gatewayWired, }); })(); }, []); if (flow.name === 'create-wizard') { return ( - + ); } if (flow.name === 'create-success') { - const wiredSuffix = flow.gatewayWired ? ` Wired to gateway "${flow.gatewayWired}" as a connector target.` : ''; return ( = { name: 'Name', @@ -26,11 +22,10 @@ const STEP_LABELS: Record = { 'data-source-type': 'Source Type', sources: 'Sources', 'add-another': 'Add another?', - gateway: 'Gateway', confirm: 'Confirm', }; -const STEPS: Step[] = ['name', 'description', 'data-source-type', 'sources', 'add-another', 'gateway', 'confirm']; +const STEPS: Step[] = ['name', 'description', 'data-source-type', 'sources', 'add-another', 'confirm']; // Each source carries its own type, so a single wizard run can mix S3 with one // or more connector types. The Flow groups by `dataSourceType` and dispatches @@ -126,9 +121,6 @@ function makeConnectorConfigSchema(pendingType: string) { }); } -const SKIP_GATEWAY_ID = '__skip__'; -const CREATE_NEW_GATEWAY_ID = '__create_new__'; - // Extract just the URI piece of S3DataSourceSchema for inline validation in // the TextInput component. const S3UriSchema = z @@ -142,14 +134,12 @@ interface AddKnowledgeBaseScreenProps { onComplete: (config: AddKnowledgeBaseConfig) => void; onExit: () => void; existingKnowledgeBaseNames: string[]; - existingGatewayNames: string[]; } export function AddKnowledgeBaseScreen({ onComplete, onExit, existingKnowledgeBaseNames, - existingGatewayNames, }: AddKnowledgeBaseScreenProps) { const [step, setStep] = useState('name'); const [name, setName] = useState(''); @@ -160,11 +150,6 @@ export function AddKnowledgeBaseScreen({ // active at the moment it was entered. const [pendingType, setPendingType] = useState('s3'); const [dataSources, setDataSources] = useState([]); - const [gateway, setGateway] = useState(undefined); - // When the user chose "Create a new gateway and attach", this holds the - // typed name. The KB Flow consumes this and creates the gateway first - // before adding the KB. Mutually exclusive with `gateway`. - const [newGatewayName, setNewGatewayName] = useState(undefined); const isPendingS3 = pendingType === 's3'; @@ -174,12 +159,8 @@ export function AddKnowledgeBaseScreen({ const isSourcesStep = step === 'sources'; const isAddAnotherStep = step === 'add-another'; const isRemoveSourceStep = step === 'remove-source'; - const isGatewayStep = step === 'gateway'; - const isNewGatewayNameStep = step === 'new-gateway-name'; const isConfirmStep = step === 'confirm'; - const hasGateways = existingGatewayNames.length > 0; - // Number of sources already captured for the *current* pendingType run, used // to label inputs ("S3 URI #2") and decide where Esc returns to. const sourcesForPendingType = useMemo( @@ -187,24 +168,6 @@ export function AddKnowledgeBaseScreen({ [dataSources, pendingType] ); - // Gateway-step picker contents adapt to whether any gateways exist: - // - Zero gateways: ["Create a new gateway and attach", "Skip — KB will be standalone"]. - // - One or more gateways: existing names + "Skip" sentinel + "Create a new gateway and attach" - // appended at the end. - const gatewayItems: SelectableItem[] = useMemo(() => { - if (!hasGateways) { - return [ - { id: CREATE_NEW_GATEWAY_ID, title: 'Create a new gateway and attach' }, - { id: SKIP_GATEWAY_ID, title: 'Skip — KB will be standalone (you can attach later)' }, - ]; - } - return [ - ...existingGatewayNames.map(g => ({ id: g, title: g })), - { id: SKIP_GATEWAY_ID, title: 'Skip — don’t wire to a gateway' }, - { id: CREATE_NEW_GATEWAY_ID, title: 'Create a new gateway and attach' }, - ]; - }, [existingGatewayNames, hasGateways]); - const dataSourceTypeNav = useListNavigation({ items: DATA_SOURCE_TYPE_OPTIONS, isActive: isDataSourceTypeStep, @@ -234,7 +197,7 @@ export function AddKnowledgeBaseScreen({ } else if (item.id === 'remove-source') { setStep('remove-source'); } else { - setStep('gateway'); + setStep('confirm'); } }, onExit: () => setStep('data-source-type'), @@ -267,25 +230,6 @@ export function AddKnowledgeBaseScreen({ onExit: () => setStep('add-another'), }); - const gatewayNav = useListNavigation({ - items: gatewayItems, - isActive: isGatewayStep, - onSelect: (item: SelectableItem) => { - if (item.id === CREATE_NEW_GATEWAY_ID) { - // Sub-step: prompt for a new gateway name. Don't create it yet — the - // Flow does the create + KB add as a single submit so the user only - // sees the gateway materialise after confirming. - setGateway(undefined); - setStep('new-gateway-name'); - return; - } - setNewGatewayName(undefined); - setGateway(item.id === SKIP_GATEWAY_ID ? undefined : item.id); - setStep('confirm'); - }, - onExit: () => setStep('add-another'), - }); - useListNavigation({ items: [{ id: 'confirm', title: 'Confirm' }], onSelect: () => @@ -293,24 +237,20 @@ export function AddKnowledgeBaseScreen({ name, dataSources, description: description || undefined, - gateway, - newGatewayName, }), - onExit: () => setStep('gateway'), + onExit: () => setStep('add-another'), isActive: isConfirmStep, }); const helpText = - isDataSourceTypeStep || isAddAnotherStep || isGatewayStep || isRemoveSourceStep + isDataSourceTypeStep || isAddAnotherStep || isRemoveSourceStep ? HELP_TEXT.NAVIGATE_SELECT : isConfirmStep ? HELP_TEXT.CONFIRM_CANCEL : HELP_TEXT.TEXT_INPUT; - // The new-gateway-name and remove-source sub-steps map onto their parents - // for the StepIndicator, mirroring the kb-id sub-step pattern in - // useAddGatewayTargetWizard. - const indicatorStep: Step = isNewGatewayNameStep ? 'gateway' : isRemoveSourceStep ? 'add-another' : step; + // The remove-source sub-step maps onto its parent for the StepIndicator. + const indicatorStep: Step = isRemoveSourceStep ? 'add-another' : step; const headerContent = ; // Confirm view: render every captured source on its own line, prefixed by @@ -333,20 +273,13 @@ export function AddKnowledgeBaseScreen({ .join('\n'); }, [dataSources, name]); - const gatewayConfirmValue = useMemo(() => { - if (newGatewayName) return `${newGatewayName} (will be created)`; - if (gateway) return `${gateway} (existing)`; - return 'none — KB will be standalone'; - }, [gateway, newGatewayName]); - const confirmFields = useMemo( () => [ { label: 'Name', value: name }, ...(description ? [{ label: 'Description', value: description }] : []), { label: `Data Sources (${dataSources.length})`, value: dataSourcesSummary }, - { label: 'Gateway', value: gatewayConfirmValue }, ], - [name, description, dataSources.length, dataSourcesSummary, gatewayConfirmValue] + [name, description, dataSources.length, dataSourcesSummary] ); return ( @@ -467,38 +400,6 @@ export function AddKnowledgeBaseScreen({ /> )} - {isGatewayStep && ( - - )} - - {isNewGatewayNameStep && ( - { - setNewGatewayName(value); - setGateway(undefined); - setStep('confirm'); - }} - onCancel={() => setStep('gateway')} - schema={GatewayNameSchema} - customValidation={value => - !existingGatewayNames.includes(value) || 'Gateway name already exists in this project' - } - /> - )} - {isConfirmStep && } diff --git a/src/cli/tui/screens/knowledge-base/__tests__/AddKnowledgeBaseFlow.test.tsx b/src/cli/tui/screens/knowledge-base/__tests__/AddKnowledgeBaseFlow.test.tsx index 12c9c01e9..928012451 100644 --- a/src/cli/tui/screens/knowledge-base/__tests__/AddKnowledgeBaseFlow.test.tsx +++ b/src/cli/tui/screens/knowledge-base/__tests__/AddKnowledgeBaseFlow.test.tsx @@ -6,20 +6,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; // ─── mocks ─────────────────────────────────────────────────────────────────── const mockKbAdd = vi.fn(); const mockKbGetRemovable = vi.fn(); -const mockGatewayAdd = vi.fn(); vi.mock('../../../../primitives/registry', () => ({ knowledgeBasePrimitive: { add: (...args: unknown[]) => mockKbAdd(...args), getRemovable: (...args: unknown[]) => mockKbGetRemovable(...args), }, - gatewayPrimitive: { - add: (...args: unknown[]) => mockGatewayAdd(...args), - }, -})); - -vi.mock('../../../hooks/useCreateMcp', () => ({ - useExistingGateways: () => ({ gateways: [] }), })); // Replace the screen with a stub that immediately invokes onComplete with a @@ -44,7 +36,6 @@ const delay = (ms = 50) => new Promise(resolve => setTimeout(resolve, ms)); beforeEach(() => { mockKbAdd.mockReset(); mockKbGetRemovable.mockReset(); - mockGatewayAdd.mockReset(); mockKbGetRemovable.mockResolvedValue([]); mockKbAdd.mockResolvedValue({ success: true, @@ -52,7 +43,6 @@ beforeEach(() => { newDataSources: ['s3-1'], gatewayWired: undefined, }); - mockGatewayAdd.mockResolvedValue({ success: true, gatewayName: 'tui-kb-gw' }); }); afterEach(() => { @@ -61,54 +51,18 @@ afterEach(() => { }); // ─── tests ─────────────────────────────────────────────────────────────────── -describe('AddKnowledgeBaseFlow — newGatewayName path', () => { - it('creates the gateway first, then adds the KB with that gateway name', async () => { - (globalThis as { __KB_FLOW_TEST_CFG?: unknown }).__KB_FLOW_TEST_CFG = { - name: 'tui-kb', - dataSources: [{ dataSourceType: 's3', value: 's3://b/' }], - newGatewayName: 'tui-kb-gw', - }; - mockKbAdd.mockResolvedValueOnce({ success: true, newDataSources: ['s3-1'], gatewayWired: 'tui-kb-gw' }); - - render(); - await delay(80); - - expect(mockGatewayAdd).toHaveBeenCalledTimes(1); - expect(mockGatewayAdd.mock.calls[0]![0]).toMatchObject({ name: 'tui-kb-gw', authorizerType: 'NONE' }); - expect(mockKbAdd).toHaveBeenCalledTimes(1); - expect(mockKbAdd.mock.calls[0]![0]).toMatchObject({ name: 'tui-kb', gateway: 'tui-kb-gw' }); - }); - - it('aborts (no KB add) if the gateway create fails', async () => { - (globalThis as { __KB_FLOW_TEST_CFG?: unknown }).__KB_FLOW_TEST_CFG = { - name: 'tui-kb', - dataSources: [{ dataSourceType: 's3', value: 's3://b/' }], - newGatewayName: 'tui-kb-gw', - }; - mockGatewayAdd.mockResolvedValueOnce({ success: false, error: new Error('boom') }); - - const { lastFrame } = render(); - await delay(80); - - expect(mockGatewayAdd).toHaveBeenCalledTimes(1); - expect(mockKbAdd).not.toHaveBeenCalled(); - expect(lastFrame() ?? '').toContain('Failed'); - }); -}); -describe('AddKnowledgeBaseFlow — Skip / standalone path (zero gateways case)', () => { - it('does not call gatewayPrimitive.add and adds the KB with no gateway', async () => { +describe('AddKnowledgeBaseFlow — standalone path', () => { + it('adds the KB with no gateway', async () => { (globalThis as { __KB_FLOW_TEST_CFG?: unknown }).__KB_FLOW_TEST_CFG = { name: 'standalone-kb', dataSources: [{ dataSourceType: 's3', value: 's3://b/' }], - // No gateway, no newGatewayName }; mockKbAdd.mockResolvedValueOnce({ success: true, newDataSources: ['s3-1'], gatewayWired: undefined }); render(); await delay(80); - expect(mockGatewayAdd).not.toHaveBeenCalled(); expect(mockKbAdd).toHaveBeenCalledTimes(1); expect(mockKbAdd.mock.calls[0]![0]).toMatchObject({ name: 'standalone-kb' }); expect(mockKbAdd.mock.calls[0]![0].gateway).toBeUndefined(); @@ -116,7 +70,7 @@ describe('AddKnowledgeBaseFlow — Skip / standalone path (zero gateways case)', }); describe('AddKnowledgeBaseFlow — existing gateway path', () => { - it('passes the existing gateway through to KB add and skips gateway create', async () => { + it('adds the KB when gateway is provided in config', async () => { (globalThis as { __KB_FLOW_TEST_CFG?: unknown }).__KB_FLOW_TEST_CFG = { name: 'kb-existing', dataSources: [{ dataSourceType: 's3', value: 's3://b/' }], @@ -127,8 +81,7 @@ describe('AddKnowledgeBaseFlow — existing gateway path', () => { render(); await delay(80); - expect(mockGatewayAdd).not.toHaveBeenCalled(); expect(mockKbAdd).toHaveBeenCalledTimes(1); - expect(mockKbAdd.mock.calls[0]![0]).toMatchObject({ name: 'kb-existing', gateway: 'g1' }); + expect(mockKbAdd.mock.calls[0]![0]).toMatchObject({ name: 'kb-existing' }); }); }); diff --git a/src/cli/tui/screens/knowledge-base/__tests__/AddKnowledgeBaseScreen.test.tsx b/src/cli/tui/screens/knowledge-base/__tests__/AddKnowledgeBaseScreen.test.tsx index 787f9623f..e08b17faa 100644 --- a/src/cli/tui/screens/knowledge-base/__tests__/AddKnowledgeBaseScreen.test.tsx +++ b/src/cli/tui/screens/knowledge-base/__tests__/AddKnowledgeBaseScreen.test.tsx @@ -15,14 +15,13 @@ const BASE_PROPS = { onComplete: vi.fn<(config: AddKnowledgeBaseConfig) => void>(), onExit: vi.fn(), existingKnowledgeBaseNames: [], - existingGatewayNames: [], }; afterEach(() => vi.restoreAllMocks()); // Helper: walk through name → description → s3 → one URI → done. -// Stops on the gateway-step picker. -async function walkToGatewayStep(stdin: ReturnType['stdin'], kbName = 'tui-kb') { +// Stops on the confirm step. +async function walkToConfirmStep(stdin: ReturnType['stdin'], kbName = 'tui-kb') { // Name step: clear default and type custom name. for (let i = 0; i < 30; i++) stdin.write(BACKSPACE); for (const ch of kbName) stdin.write(ch); @@ -51,187 +50,52 @@ async function walkToGatewayStep(stdin: ReturnType['stdin'], kbNa await delay(); } -describe('AddKnowledgeBaseScreen — gateway step always shown', () => { - it('zero gateways: gateway step shows Create-new + Skip (no other items)', async () => { +describe('AddKnowledgeBaseScreen — confirm step', () => { + it('done navigates directly to confirm showing name and data sources', async () => { const { lastFrame, stdin } = render(); - await walkToGatewayStep(stdin); + await walkToConfirmStep(stdin); const frame = lastFrame() ?? ''; - expect(frame).toContain('Wire this knowledge base to a gateway?'); - expect(frame).toContain('Create a new gateway and attach'); - expect(frame).toContain('Skip — KB will be standalone'); - expect(frame).toContain('No gateways exist in this project yet'); + expect(frame).toContain('Name:'); + expect(frame).toContain('tui-kb'); + expect(frame).toContain('Data Sources'); + expect(frame).toContain('s3://my-bucket/docs/'); }); - it('zero gateways: picking Skip goes to confirm with "Gateway: none — KB will be standalone"', async () => { - const { lastFrame, stdin } = render(); - await walkToGatewayStep(stdin); - - // Move from "Create new" (index 0) down to "Skip" (index 1) - stdin.write(DOWN_ARROW); - await delay(); - stdin.write(ENTER); - await delay(); - - const frame = lastFrame() ?? ''; - expect(frame).toContain('Gateway:'); - expect(frame).toContain('none — KB will be standalone'); - }); - - it('zero gateways: picking Create-new advances to a name input defaulted to "${kbName}-gw"', async () => { - const { lastFrame, stdin } = render(); - await walkToGatewayStep(stdin, 'mykb'); - - // "Create a new gateway and attach" is index 0 in the zero-gateway picker - stdin.write(ENTER); - await delay(); - - const frame = lastFrame() ?? ''; - expect(frame).toContain('New gateway name'); - expect(frame).toContain('mykb-gw'); - }); - - it('zero gateways: full flow Create-new → submit emits newGatewayName, no gateway', async () => { + it('full flow submit emits correct config', async () => { const onComplete = vi.fn<(config: AddKnowledgeBaseConfig) => void>(); const { stdin } = render(); - await walkToGatewayStep(stdin, 'tui-kb'); + await walkToConfirmStep(stdin, 'tui-kb'); - // Pick Create-new (index 0) - stdin.write(ENTER); - await delay(); - // Accept default name "tui-kb-gw" - stdin.write(ENTER); - await delay(); // Confirm stdin.write(ENTER); await delay(); expect(onComplete).toHaveBeenCalledTimes(1); const cfg = onComplete.mock.calls[0]![0]; - expect(cfg.newGatewayName).toBe('tui-kb-gw'); - expect(cfg.gateway).toBeUndefined(); expect(cfg.name).toBe('tui-kb'); + expect(cfg.dataSources).toHaveLength(1); + expect(cfg.dataSources[0]!.dataSourceType).toBe('s3'); + expect(cfg.dataSources[0]!.value).toBe('s3://my-bucket/docs/'); }); - it('zero gateways: confirm view shows "Gateway: tui-kb-gw (will be created)"', async () => { - const { lastFrame, stdin } = render(); - await walkToGatewayStep(stdin, 'tui-kb'); - - stdin.write(ENTER); // Create-new - await delay(); - stdin.write(ENTER); // accept default - await delay(); - - const frame = lastFrame() ?? ''; - expect(frame).toContain('tui-kb-gw (will be created)'); - }); - - it('rejects invalid gateway names with the schema error', async () => { + it('Esc from confirm returns to add-another step', async () => { const { lastFrame, stdin } = render(); - await walkToGatewayStep(stdin, 'tui-kb'); - - stdin.write(ENTER); // Create-new - await delay(); - // Clear default and type invalid name - for (let i = 0; i < 30; i++) stdin.write(BACKSPACE); - for (const ch of 'bad name!') stdin.write(ch); - await delay(); - stdin.write(ENTER); - await delay(); - - const frame = lastFrame() ?? ''; - expect(frame).toMatch(/alphanumeric with optional hyphens|invalid|error/i); - }); -}); - -describe('AddKnowledgeBaseScreen — at-least-one-gateway path', () => { - const PROPS_WITH_GW = { ...BASE_PROPS, existingGatewayNames: ['g1', 'g2'] }; - - it('shows existing names + Skip + Create-new in that order', async () => { - const { lastFrame, stdin } = render(); - await walkToGatewayStep(stdin); - const frame = lastFrame() ?? ''; - expect(frame).toContain('g1'); - expect(frame).toContain('g2'); - expect(frame).toContain('Skip'); - expect(frame).toContain('Create a new gateway and attach'); - }); + await walkToConfirmStep(stdin); - it('selecting an existing gateway emits gateway=g1, no newGatewayName', async () => { - const onComplete = vi.fn<(config: AddKnowledgeBaseConfig) => void>(); - const { stdin } = render(); - await walkToGatewayStep(stdin); - - // g1 is index 0 - stdin.write(ENTER); - await delay(); - // Confirm - stdin.write(ENTER); - await delay(); - - expect(onComplete).toHaveBeenCalledTimes(1); - const cfg = onComplete.mock.calls[0]![0]; - expect(cfg.gateway).toBe('g1'); - expect(cfg.newGatewayName).toBeUndefined(); - }); - - it('Create-new appended at end is reachable; choosing it advances to the name input', async () => { - const { lastFrame, stdin } = render(); - await walkToGatewayStep(stdin, 'kb'); - // Items: g1(0), g2(1), Skip(2), Create-new(3). Down 3 times. - stdin.write(DOWN_ARROW); - await delay(); - stdin.write(DOWN_ARROW); - await delay(); - stdin.write(DOWN_ARROW); - await delay(); - stdin.write(ENTER); - await delay(); - - const frame = lastFrame() ?? ''; - expect(frame).toContain('New gateway name'); - expect(frame).toContain('kb-gw'); - }); - - it('confirm shows "Gateway: g2 (existing)" when an existing gateway picked', async () => { - const { lastFrame, stdin } = render(); - await walkToGatewayStep(stdin); - - // Move to g2 - stdin.write(DOWN_ARROW); - await delay(); - stdin.write(ENTER); - await delay(); - - const frame = lastFrame() ?? ''; - expect(frame).toContain('g2 (existing)'); - }); - - it('Esc from new-gateway-name returns to the gateway picker', async () => { - const { lastFrame, stdin } = render(); - await walkToGatewayStep(stdin); - - // Pick Create-new (index 3) - for (let i = 0; i < 3; i++) { - stdin.write(DOWN_ARROW); - await delay(); - } - stdin.write(ENTER); - await delay(); - // Should be on new-gateway-name. Esc back. stdin.write(ESCAPE); await delay(); const frame = lastFrame() ?? ''; - expect(frame).toContain('Wire this knowledge base to a gateway?'); + expect(frame).toContain('Add another data source'); }); }); describe('AddKnowledgeBaseScreen — step indicator', () => { - it('always shows the Gateway label in the step list, even with zero gateways', async () => { + it('shows Confirm label in the step list', async () => { const { lastFrame } = render(); await delay(); - expect(lastFrame() ?? '').toContain('Gateway'); + expect(lastFrame() ?? '').toContain('Confirm'); }); }); // Suppress unused imports from helper — keep references silent diff --git a/src/cli/tui/screens/knowledge-base/types.ts b/src/cli/tui/screens/knowledge-base/types.ts index 8a3bcaf21..88a40d47c 100644 --- a/src/cli/tui/screens/knowledge-base/types.ts +++ b/src/cli/tui/screens/knowledge-base/types.ts @@ -26,18 +26,4 @@ export interface AddKnowledgeBaseConfig { * first call creates the KB, subsequent calls append. */ dataSources: CapturedDataSource[]; - /** - * Wave 2: optional name of an existing gateway to wire the new KB to as a - * connector target. Undefined when the user skips gateway-wiring or chose - * to create a new gateway (see `newGatewayName`). Mutually exclusive with - * `newGatewayName`. - */ - gateway?: string; - /** - * When the user chose "Create a new gateway and attach", this is the name - * of the gateway to create. The Flow creates the gateway first, then - * passes this name as the gateway for the KB add. Mutually exclusive with - * `gateway`. - */ - newGatewayName?: string; } diff --git a/src/cli/tui/screens/mcp/AddGatewayTargetFlow.tsx b/src/cli/tui/screens/mcp/AddGatewayTargetFlow.tsx index 9800aca6e..025759312 100644 --- a/src/cli/tui/screens/mcp/AddGatewayTargetFlow.tsx +++ b/src/cli/tui/screens/mcp/AddGatewayTargetFlow.tsx @@ -139,13 +139,7 @@ export function AddGatewayTargetFlow({ void gatewayTargetPrimitive .createConnectorGatewayTarget(config) .then((result: { toolName: string }) => { - // For single-KB Retrieve adds, the primitive also upserts the - // gateway's shared agentic-retrieve target. Surface that to the user. - const detail = - config.connectorId === 'bedrock-knowledge-bases' - ? `Also wired KB '${config.knowledgeBaseId}' into '${config.gateway}-agentic' (bedrock-agentic-retrieve fan-out)` - : undefined; - setFlow({ name: 'create-success', toolName: result.toolName, projectPath: '', detail }); + setFlow({ name: 'create-success', toolName: result.toolName, projectPath: '' }); }) .catch((err: unknown) => { setFlow({ name: 'error', message: err instanceof Error ? err.message : 'Unknown error' }); diff --git a/src/cli/tui/screens/mcp/AddGatewayTargetScreen.tsx b/src/cli/tui/screens/mcp/AddGatewayTargetScreen.tsx index dcf29bb8e..7721d0d33 100644 --- a/src/cli/tui/screens/mcp/AddGatewayTargetScreen.tsx +++ b/src/cli/tui/screens/mcp/AddGatewayTargetScreen.tsx @@ -24,7 +24,6 @@ import { getOutboundAuthOptions, } from './types'; import { useAddGatewayTargetWizard } from './useAddGatewayTargetWizard'; -import { isGatedFeaturesEnabled } from '@/cli/feature-flags'; import { Box, Text } from 'ink'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; @@ -114,17 +113,17 @@ export function AddGatewayTargetScreen({ const isAuthStep = isOutboundAuthStep || isApiGatewayAuthStep; const noGatewaysAvailable = isGatewayStep && existingGateways.length === 0; - // Auto-select the gateway for webSearch targets when there's exactly one option. + // Auto-select the gateway for web-search connector when there's exactly one option. useEffect(() => { if ( isGatewayStep && - wizard.config.targetType === 'webSearch' && + wizard.config.connectorId === 'web-search' && existingGateways.length === 1 && !wizard.config.gateway ) { wizard.setGateway(existingGateways[0]!); } - }, [isGatewayStep, wizard.config.targetType, existingGateways, wizard.config.gateway, wizard.setGateway]); + }, [isGatewayStep, wizard.config.connectorId, existingGateways, wizard.config.gateway, wizard.setGateway]); // ── Selectable item lists ── const isNonMcpTarget = wizard.config.targetType === 'httpRuntime' || wizard.config.targetType === 'passthrough'; @@ -153,16 +152,7 @@ export function AddGatewayTargetScreen({ [existingGateways, mcpGatewayNames, isHttpRuntimeTarget] ); const targetTypeItems: SelectableItem[] = useMemo( - () => - TARGET_TYPE_OPTIONS.map(o => { - const gated = o.id === 'webSearch' && !isGatedFeaturesEnabled(); - return { - id: o.id, - title: o.title, - description: gated ? 'Coming soon' : o.description, - disabled: gated, - }; - }), + () => TARGET_TYPE_OPTIONS.map(o => ({ id: o.id, title: o.title, description: o.description })), [] ); const outboundAuthItems: SelectableItem[] = useMemo( @@ -237,7 +227,11 @@ export function AddGatewayTargetScreen({ items: targetTypeItems, onSelect: item => { if ((item as SelectableItem & { disabled?: boolean }).disabled) return; - wizard.setTargetType(item.id as GatewayTargetType); + if (item.id === 'webSearch') { + wizard.setTargetType('connector', 'web-search'); + } else { + wizard.setTargetType(item.id as GatewayTargetType); + } }, onExit: () => wizard.goBack(), isActive: isTargetTypeStep, @@ -425,24 +419,23 @@ export function AddGatewayTargetScreen({ endpoint: c.endpoint, outboundAuth: c.outboundAuth, }); - } else if (c.targetType === 'webSearch') { - onComplete({ - targetType: 'webSearch', - name: c.name, - gateway: c.gateway!, - ...(c.excludeDomains && c.excludeDomains.length > 0 ? { excludeDomains: c.excludeDomains } : {}), - }); } else if (c.targetType === 'connector') { - // KB connector path. `bedrock-agentic-retrieve` is gateway-managed by - // the Add Knowledge Base flow, so the TUI only emits - // `bedrock-knowledge-bases` here. - onComplete({ - targetType: 'connector', - connectorId: 'bedrock-knowledge-bases', - name: c.name, - gateway: c.gateway!, - knowledgeBaseId: c.knowledgeBaseId!, - }); + if (c.connectorId === 'web-search') { + onComplete({ + targetType: 'webSearch', + name: c.name, + gateway: c.gateway!, + ...(c.excludeDomains && c.excludeDomains.length > 0 ? { excludeDomains: c.excludeDomains } : {}), + }); + } else { + onComplete({ + targetType: 'connector', + connectorId: 'bedrock-knowledge-bases', + name: c.name, + gateway: c.gateway!, + knowledgeBaseId: c.knowledgeBaseId!, + }); + } } else if (c.targetType === 'passthrough') { onComplete({ targetType: 'passthrough', @@ -842,7 +835,9 @@ export function AddGatewayTargetScreen({ { label: 'Target Type', value: - TARGET_TYPE_OPTIONS.find(o => o.id === wizard.config.targetType)?.title ?? + (wizard.config.targetType === 'connector' && wizard.config.connectorId === 'web-search' + ? TARGET_TYPE_OPTIONS.find(o => o.id === 'webSearch')?.title + : TARGET_TYPE_OPTIONS.find(o => o.id === wizard.config.targetType)?.title) ?? wizard.config.targetType ?? '', }, @@ -869,10 +864,10 @@ export function AddGatewayTargetScreen({ ...(wizard.config.endpoint ? [{ label: 'Runtime Endpoint', value: wizard.config.endpoint }] : []), ] : []), - ...(wizard.config.targetType === 'webSearch' + ...(wizard.config.connectorId === 'web-search' ? [{ label: 'Exclude domains', value: wizard.config.excludeDomains?.join(', ') ?? '(none)' }] : []), - ...(wizard.config.targetType === 'connector' + ...(wizard.config.targetType === 'connector' && wizard.config.connectorId !== 'web-search' ? [ { label: 'Connector', value: wizard.config.connectorId ?? 'bedrock-knowledge-bases' }, { label: 'Knowledge Base', value: wizard.config.knowledgeBaseId ?? '' }, diff --git a/src/cli/tui/screens/mcp/types.ts b/src/cli/tui/screens/mcp/types.ts index 482b47ff7..6f1cf7cea 100644 --- a/src/cli/tui/screens/mcp/types.ts +++ b/src/cli/tui/screens/mcp/types.ts @@ -144,11 +144,9 @@ export interface GatewayTargetWizardState { /** Knowledge Base reference for connector targets — either a project KB name or a literal 10-char KB ID. */ knowledgeBaseId?: string; /** - * Connector identifier when targetType is 'connector'. Only - * `bedrock-knowledge-bases` is exposed in the TUI; `bedrock-agentic-retrieve` - * is gateway-managed by the Add Knowledge Base flow. + * Connector identifier when targetType is 'connector'. */ - connectorId?: 'bedrock-knowledge-bases' | 'bedrock-agentic-retrieve'; + connectorId?: 'bedrock-knowledge-bases' | 'web-search'; /** Passthrough endpoint URL for passthrough targets */ passthroughEndpoint?: string; /** Passthrough protocol type for passthrough targets */ @@ -242,15 +240,7 @@ export interface BedrockKnowledgeBasesConnectorTargetConfig extends ConnectorTar knowledgeBaseId: string; } -export interface BedrockAgenticRetrieveConnectorTargetConfig extends ConnectorTargetConfigBase { - connectorId: 'bedrock-agentic-retrieve'; - /** Fan-out: project KB names and/or literal 10-char external KB IDs. */ - knowledgeBaseIds: string[]; -} - -export type ConnectorTargetConfig = - | BedrockKnowledgeBasesConnectorTargetConfig - | BedrockAgenticRetrieveConnectorTargetConfig; +export type ConnectorTargetConfig = BedrockKnowledgeBasesConnectorTargetConfig; export interface PassthroughTargetConfig { targetType: 'passthrough'; diff --git a/src/cli/tui/screens/mcp/useAddGatewayTargetWizard.ts b/src/cli/tui/screens/mcp/useAddGatewayTargetWizard.ts index 525ffe456..2d7af3f36 100644 --- a/src/cli/tui/screens/mcp/useAddGatewayTargetWizard.ts +++ b/src/cli/tui/screens/mcp/useAddGatewayTargetWizard.ts @@ -57,10 +57,11 @@ export function useAddGatewayTargetWizard( baseSteps.push('runtime', 'runtime-endpoint', 'gateway', 'outbound-auth'); break; case 'connector': - // Connector (Knowledge Base) flow: select a KB (project name or - // literal 10-char ID), then attach to a gateway. No outbound auth — - // connector targets are managed by the gateway IAM role. - baseSteps.push('kb-select', 'gateway'); + if (config.connectorId === 'web-search') { + baseSteps.push('gateway', 'exclude-domains'); + } else { + baseSteps.push('kb-select', 'gateway'); + } break; case 'passthrough': baseSteps.push( @@ -73,11 +74,6 @@ export function useAddGatewayTargetWizard( 'signing-region' ); break; - case 'webSearch': - // Amazon Web Search flow: pick a gateway, optionally specify domains - // to exclude. No outbound auth — managed by the gateway IAM role. - baseSteps.push('gateway', 'exclude-domains'); - break; case 'mcpServer': default: baseSteps.push('endpoint', 'gateway', 'outbound-auth'); @@ -86,7 +82,7 @@ export function useAddGatewayTargetWizard( baseSteps.push('confirm'); } return baseSteps; - }, [config.targetType]); + }, [config.targetType, config.connectorId]); // The 'kb-id' step is a sub-step of 'kb-select' for manual literal-KB-ID entry. // It is not part of the canonical step list, so map it onto kb-select for @@ -127,20 +123,14 @@ export function useAddGatewayTargetWizard( [goToNextStep] ); - const setTargetType = useCallback((targetType: GatewayTargetType) => { - // KB connector targets default to 'bedrock-knowledge-bases' for the TUI; - // 'bedrock-agentic-retrieve' is gateway-managed by the Add Knowledge Base - // flow and not directly exposed here. webSearch targets carry no connectorId. - const connectorIdDefault = targetType === 'connector' ? ('bedrock-knowledge-bases' as const) : undefined; + const setTargetType = useCallback((targetType: GatewayTargetType, connectorId?: string) => { + const resolvedConnectorId = targetType === 'connector' ? (connectorId ?? 'bedrock-knowledge-bases') : undefined; setConfig(c => ({ ...c, targetType, - ...(connectorIdDefault ? { connectorId: connectorIdDefault } : { connectorId: undefined }), - ...(targetType !== 'webSearch' ? { excludeDomains: undefined } : {}), + connectorId: resolvedConnectorId, + ...(resolvedConnectorId !== 'web-search' ? { excludeDomains: undefined } : {}), })); - // Cannot use goToNextStep() here — config.targetType is changing, which triggers - // useMemo to recompute steps, but goToNextStep captures the OLD steps via closure. - // Must explicitly set the first type-specific step. switch (targetType) { case 'apiGateway': setStep('rest-api-id'); @@ -156,14 +146,11 @@ export function useAddGatewayTargetWizard( setStep('runtime'); break; case 'connector': - setStep('kb-select'); + setStep(resolvedConnectorId === 'web-search' ? 'gateway' : 'kb-select'); break; case 'passthrough': setStep('passthrough-endpoint'); break; - case 'webSearch': - setStep('gateway'); - break; case 'mcpServer': default: setStep('endpoint'); diff --git a/src/cli/tui/screens/web-search/AddWebSearchFlow.tsx b/src/cli/tui/screens/web-search/AddWebSearchFlow.tsx deleted file mode 100644 index 09b27c3b7..000000000 --- a/src/cli/tui/screens/web-search/AddWebSearchFlow.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { gatewayTargetPrimitive } from '../../../primitives/registry'; -import { ErrorPrompt } from '../../components'; -import { useExistingGateways, useExistingToolNames } from '../../hooks/useCreateMcp'; -import { AddSuccessScreen } from '../add/AddSuccessScreen'; -import { AddWebSearchScreen } from './AddWebSearchScreen'; -import type { AddWebSearchConfig } from './types'; -import React, { useCallback, useEffect, useState } from 'react'; - -type FlowState = - | { name: 'create-wizard' } - | { name: 'create-success'; toolName: string; gateway: string; excludeDomains?: string[] } - | { name: 'error'; message: string }; - -interface AddWebSearchFlowProps { - isInteractive?: boolean; - onExit: () => void; - onBack: () => void; - onDev?: () => void; - onDeploy?: () => void; -} - -export function AddWebSearchFlow({ isInteractive = true, onExit, onBack, onDev, onDeploy }: AddWebSearchFlowProps) { - const [flow, setFlow] = useState({ name: 'create-wizard' }); - const { gateways: existingGateways } = useExistingGateways(); - const { toolNames: existingToolNames } = useExistingToolNames(); - - // In non-interactive mode, exit after success. - useEffect(() => { - if (!isInteractive && flow.name === 'create-success') { - onExit(); - } - }, [isInteractive, flow.name, onExit]); - - const handleComplete = useCallback((config: AddWebSearchConfig) => { - void gatewayTargetPrimitive - .createWebSearchGatewayTarget({ - targetType: 'webSearch', - name: config.name, - gateway: config.gateway, - ...(config.excludeDomains && config.excludeDomains.length > 0 ? { excludeDomains: config.excludeDomains } : {}), - }) - .then((result: { toolName: string }) => { - setFlow({ - name: 'create-success', - toolName: result.toolName, - gateway: config.gateway, - excludeDomains: config.excludeDomains, - }); - }) - .catch((err: unknown) => { - setFlow({ name: 'error', message: err instanceof Error ? err.message : 'Unknown error' }); - }); - }, []); - - if (flow.name === 'create-wizard') { - return ( - - ); - } - if (flow.name === 'create-success') { - const excludeSuffix = - flow.excludeDomains && flow.excludeDomains.length > 0 - ? ` Excluded domains: ${flow.excludeDomains.join(', ')}.` - : ''; - return ( - - ); - } - if (flow.name === 'error') { - return ( - - ); - } - return null; -} diff --git a/src/cli/tui/screens/web-search/AddWebSearchScreen.tsx b/src/cli/tui/screens/web-search/AddWebSearchScreen.tsx deleted file mode 100644 index fd741c545..000000000 --- a/src/cli/tui/screens/web-search/AddWebSearchScreen.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import { ToolNameSchema } from '../../../../schema'; -import { ConfirmReview, Panel, Screen, StepIndicator, TextInput, WizardSelect } from '../../components'; -import type { SelectableItem } from '../../components'; -import { HELP_TEXT } from '../../constants'; -import { useListNavigation } from '../../hooks'; -import { generateUniqueName } from '../../utils'; -import type { AddWebSearchConfig } from './types'; -import { Box, Text } from 'ink'; -import React, { useMemo, useState } from 'react'; - -type Step = 'name' | 'gateway' | 'exclude-domains' | 'confirm'; - -const STEP_LABELS: Record = { - name: 'Name', - gateway: 'Gateway', - 'exclude-domains': 'Exclude domains', - confirm: 'Confirm', -}; - -const STEPS: Step[] = ['name', 'gateway', 'exclude-domains', 'confirm']; - -interface AddWebSearchScreenProps { - onComplete: (config: AddWebSearchConfig) => void; - onExit: () => void; - existingGatewayNames: string[]; - existingToolNames: string[]; -} - -export function AddWebSearchScreen({ - onComplete, - onExit, - existingGatewayNames, - existingToolNames, -}: AddWebSearchScreenProps) { - const [step, setStep] = useState('name'); - const [name, setName] = useState(''); - const [gateway, setGateway] = useState(undefined); - const [excludeDomains, setExcludeDomains] = useState(undefined); - - const isNameStep = step === 'name'; - const isGatewayStep = step === 'gateway'; - const isExcludeDomainsStep = step === 'exclude-domains'; - const isConfirmStep = step === 'confirm'; - - const noGatewaysAvailable = isGatewayStep && existingGatewayNames.length === 0; - - const gatewayItems: SelectableItem[] = useMemo( - () => existingGatewayNames.map(g => ({ id: g, title: g })), - [existingGatewayNames] - ); - - const gatewayNav = useListNavigation({ - items: gatewayItems, - isActive: isGatewayStep && !noGatewaysAvailable, - onSelect: (item: SelectableItem) => { - setGateway(item.id); - setStep('exclude-domains'); - }, - onExit: () => setStep('name'), - }); - - useListNavigation({ - items: [{ id: 'confirm', title: 'Confirm' }], - onSelect: () => onComplete({ name, gateway: gateway!, excludeDomains }), - onExit: () => setStep('exclude-domains'), - isActive: isConfirmStep, - }); - - const helpText = isGatewayStep - ? HELP_TEXT.NAVIGATE_SELECT - : isConfirmStep - ? HELP_TEXT.CONFIRM_CANCEL - : HELP_TEXT.TEXT_INPUT; - - const headerContent = ; - - const confirmFields = useMemo( - () => [ - { label: 'Name', value: name }, - { label: 'Gateway', value: gateway ?? '' }, - { - label: 'Exclude domains', - value: excludeDomains && excludeDomains.length > 0 ? excludeDomains.join(', ') : '(none)', - }, - ], - [name, gateway, excludeDomains] - ); - - return ( - - - {isNameStep && ( - { - setName(value); - setStep('gateway'); - }} - onCancel={onExit} - schema={ToolNameSchema} - customValidation={value => !existingToolNames.includes(value) || 'Target name already exists'} - /> - )} - - {isGatewayStep && noGatewaysAvailable && } - - {isGatewayStep && !noGatewaysAvailable && ( - - )} - - {isExcludeDomainsStep && ( - { - const domains = value - .split(',') - .map(d => d.trim()) - .filter(d => d.length > 0); - setExcludeDomains(domains.length > 0 ? domains : undefined); - setStep('confirm'); - }} - onCancel={() => setStep('gateway')} - /> - )} - - {isConfirmStep && } - - - ); -} - -function NoGatewaysMessage() { - return ( - - No gateways found - Run `agentcore add gateway` first, then re-run this command. - - Esc back - - - ); -} diff --git a/src/cli/tui/screens/web-search/__tests__/AddWebSearchScreen.test.tsx b/src/cli/tui/screens/web-search/__tests__/AddWebSearchScreen.test.tsx deleted file mode 100644 index 0bcdf9a37..000000000 --- a/src/cli/tui/screens/web-search/__tests__/AddWebSearchScreen.test.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { AddWebSearchScreen } from '../AddWebSearchScreen'; -import type { AddWebSearchConfig } from '../types'; -import { render } from 'ink-testing-library'; -import React from 'react'; -import { afterEach, describe, expect, it, vi } from 'vitest'; - -const ENTER = '\r'; -const ESCAPE = '\x1B'; -const BACKSPACE = '\x7f'; -const delay = (ms = 50) => new Promise(resolve => setTimeout(resolve, ms)); - -function makeProps(overrides: Partial> = {}) { - return { - onComplete: vi.fn<(config: AddWebSearchConfig) => void>(), - onExit: vi.fn(), - existingGatewayNames: [], - existingToolNames: [], - ...overrides, - }; -} - -// Walk past the name step by accepting the default and pressing Enter. -async function submitName(stdin: ReturnType['stdin']) { - stdin.write(ENTER); - await delay(); -} - -afterEach(() => vi.restoreAllMocks()); - -describe('AddWebSearchScreen — Escape navigation', () => { - it('Escape on the no-gateways view calls onExit (the only step where Screen owns Esc)', async () => { - const props = makeProps({ existingGatewayNames: [] }); - const { lastFrame, stdin } = render(); - - await submitName(stdin); - expect(lastFrame() ?? '').toContain('No gateways found'); - - stdin.write(ESCAPE); - await delay(); - - expect(props.onExit).toHaveBeenCalledTimes(1); - }); - - it('Escape on the gateway step (with gateways) goes back to name, does NOT call onExit', async () => { - const props = makeProps({ existingGatewayNames: ['gw1'] }); - const { lastFrame, stdin } = render(); - - await submitName(stdin); - expect(lastFrame() ?? '').toContain('Attach to which gateway?'); - - stdin.write(ESCAPE); - await delay(); - - expect(props.onExit).not.toHaveBeenCalled(); - // Back at the name step: the name input is mounted again. - expect(lastFrame() ?? '').toContain('Web search target name'); - }); - - it('Escape on the exclude-domains step goes back to gateway, does NOT call onExit', async () => { - const props = makeProps({ existingGatewayNames: ['gw1'] }); - const { lastFrame, stdin } = render(); - - await submitName(stdin); - // Pick gw1 (single item, already selected), advance to exclude-domains. - stdin.write(ENTER); - await delay(); - expect(lastFrame() ?? '').toContain('Exclude domains'); - - stdin.write(ESCAPE); - await delay(); - - expect(props.onExit).not.toHaveBeenCalled(); - expect(lastFrame() ?? '').toContain('Attach to which gateway?'); - }); - - it('Escape on the confirm step goes back to exclude-domains, does NOT call onExit', async () => { - const props = makeProps({ existingGatewayNames: ['gw1'] }); - const { lastFrame, stdin } = render(); - - await submitName(stdin); - stdin.write(ENTER); // pick gw1 - await delay(); - stdin.write(ENTER); // submit empty exclude-domains, advance to confirm - await delay(); - expect(lastFrame() ?? '').toContain('Confirm'); - - stdin.write(ESCAPE); - await delay(); - - expect(props.onExit).not.toHaveBeenCalled(); - expect(lastFrame() ?? '').toContain('Exclude domains'); - }); - - it('Escape on the name step calls onExit', async () => { - const props = makeProps({ existingGatewayNames: ['gw1'] }); - const { stdin } = render(); - - // Clear the default value first so TextInput's onCancel fires onExit - // rather than just clearing input. - for (let i = 0; i < 30; i++) stdin.write(BACKSPACE); - await delay(); - stdin.write(ESCAPE); - await delay(); - - expect(props.onExit).toHaveBeenCalled(); - }); -}); diff --git a/src/cli/tui/screens/web-search/index.ts b/src/cli/tui/screens/web-search/index.ts deleted file mode 100644 index 94c25c5e4..000000000 --- a/src/cli/tui/screens/web-search/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { AddWebSearchFlow } from './AddWebSearchFlow'; -export type { AddWebSearchConfig } from './types'; diff --git a/src/cli/tui/screens/web-search/types.ts b/src/cli/tui/screens/web-search/types.ts deleted file mode 100644 index a2dadcb4b..000000000 --- a/src/cli/tui/screens/web-search/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Captured by the AddWebSearchScreen wizard and passed to the Flow, which - * dispatches to gatewayTargetPrimitive.createWebSearchGatewayTarget(). - */ -export interface AddWebSearchConfig { - name: string; - gateway: string; - /** Optional list of domains to exclude from search results. */ - excludeDomains?: string[]; -} diff --git a/src/schema/llm-compacted/mcp.ts b/src/schema/llm-compacted/mcp.ts index cfdd94e6b..9e8580bec 100644 --- a/src/schema/llm-compacted/mcp.ts +++ b/src/schema/llm-compacted/mcp.ts @@ -45,18 +45,12 @@ interface AgentCoreGatewayTarget { connectorId?: ConnectorId; /** * For bedrock-knowledge-bases connector targets — a project KB name or a - * literal 10-char external KB ID. Mutually exclusive with `knowledgeBaseIds`. + * literal 10-char external KB ID. */ knowledgeBaseId?: string; /** - * For bedrock-agentic-retrieve connector targets — fan-out list of project - * KB names or literal 10-char external KB IDs. Mutually exclusive with - * `knowledgeBaseId`. - */ - knowledgeBaseIds?: string[]; - /** - * For `webSearch` target type only — domains to exclude from search results. - * Maps to the connector's `domainFilter.exclude` parameterValue at synth. + * For connector targets with connectorId 'web-search'. Domains to exclude + * from search results. Maps to `domainFilter.exclude` parameterValue at synth. */ excludeDomains?: string[]; } @@ -203,9 +197,8 @@ type GatewayTargetType = | 'lambdaFunctionArn' | 'httpRuntime' | 'connector' - | 'passthrough' - | 'webSearch'; -type ConnectorId = 'bedrock-knowledge-bases' | 'bedrock-agentic-retrieve'; + | 'passthrough'; +type ConnectorId = 'bedrock-knowledge-bases' | 'web-search'; type PythonRuntime = 'PYTHON_3_10' | 'PYTHON_3_11' | 'PYTHON_3_12' | 'PYTHON_3_13' | 'PYTHON_3_14'; type NodeRuntime = 'NODE_18' | 'NODE_20' | 'NODE_22'; type NetworkMode = 'PUBLIC' | 'VPC'; diff --git a/src/schema/schemas/__tests__/agentcore-project.test.ts b/src/schema/schemas/__tests__/agentcore-project.test.ts index 3796f85cf..1034a370c 100644 --- a/src/schema/schemas/__tests__/agentcore-project.test.ts +++ b/src/schema/schemas/__tests__/agentcore-project.test.ts @@ -711,7 +711,7 @@ describe('AgentCoreProjectSpecSchema', () => { name: 'kb-target', targetType: 'connector', connectorId: 'bedrock-knowledge-bases', - knowledgeBaseId: 'ABCDEFGHIJ', + configurations: [{ name: 'Retrieve', parameterValues: { knowledgeBaseId: 'ABCDEFGHIJ' } }], }, ], }, diff --git a/src/schema/schemas/__tests__/mcp.test.ts b/src/schema/schemas/__tests__/mcp.test.ts index 392e3bdca..21a1014a4 100644 --- a/src/schema/schemas/__tests__/mcp.test.ts +++ b/src/schema/schemas/__tests__/mcp.test.ts @@ -33,7 +33,6 @@ describe('GatewayTargetTypeSchema', () => { 'httpRuntime', 'connector', 'passthrough', - 'webSearch', ]); }); @@ -1213,150 +1212,38 @@ describe('AgentCoreGatewayTargetSchema with httpRuntime', () => { }); }); -describe('AgentCoreGatewayTargetSchema with webSearch', () => { - it('accepts a minimal webSearch target', () => { +describe('AgentCoreGatewayTargetSchema with web-search connector', () => { + it('accepts a minimal web-search connector target', () => { const result = AgentCoreGatewayTargetSchema.safeParse({ name: 'web-search', - targetType: 'webSearch', + targetType: 'connector', + connectorId: 'web-search', }); expect(result.success).toBe(true); }); - it('accepts a webSearch target with excludeDomains', () => { + it('accepts a web-search connector target with configurations', () => { const result = AgentCoreGatewayTargetSchema.safeParse({ name: 'web-search', - targetType: 'webSearch', - excludeDomains: ['internal.example.com', 'staging.example.com'], + targetType: 'connector', + connectorId: 'web-search', + configurations: [ + { + name: 'WebSearch', + parameterValues: { domainFilter: { exclude: ['internal.example.com', 'staging.example.com'] } }, + }, + ], }); expect(result.success).toBe(true); }); - it('rejects an empty excludeDomains array', () => { - const result = AgentCoreGatewayTargetSchema.safeParse({ - name: 'web-search', - targetType: 'webSearch', - excludeDomains: [], - }); - expect(result.success).toBe(false); - }); - - it('rejects a webSearch target with compute', () => { - const result = AgentCoreGatewayTargetSchema.safeParse({ - name: 'web-search', - targetType: 'webSearch', - compute: { - host: 'Lambda', - implementation: { language: 'Python', path: 'tools', handler: 'h' }, - pythonVersion: 'PYTHON_3_12', - }, - }); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues.some(i => i.message.includes('compute'))).toBe(true); - } - }); - - it('rejects a webSearch target with endpoint', () => { - const result = AgentCoreGatewayTargetSchema.safeParse({ - name: 'web-search', - targetType: 'webSearch', - endpoint: 'https://example.com/mcp', - }); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues.some(i => i.message.includes('endpoint'))).toBe(true); - } - }); - - it('rejects a webSearch target with apiGateway config', () => { - const result = AgentCoreGatewayTargetSchema.safeParse({ - name: 'web-search', - targetType: 'webSearch', - apiGateway: { - restApiId: 'abc123', - stage: 'prod', - apiGatewayToolConfiguration: { toolFilters: [{ filterPath: '/*', methods: ['GET'] }] }, - }, - }); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues.some(i => i.message.includes('apiGateway'))).toBe(true); - } - }); - - it('rejects a webSearch target with lambdaFunctionArn config', () => { - const result = AgentCoreGatewayTargetSchema.safeParse({ - name: 'web-search', - targetType: 'webSearch', - lambdaFunctionArn: { - lambdaArn: 'arn:aws:lambda:us-east-1:123456789012:function:my-func', - toolSchemaFile: './tools.json', - }, - }); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues.some(i => i.message.includes('lambdaFunctionArn'))).toBe(true); - } - }); - - it('rejects a webSearch target with schemaSource', () => { - const result = AgentCoreGatewayTargetSchema.safeParse({ - name: 'web-search', - targetType: 'webSearch', - schemaSource: { inline: { path: './schema.json' } }, - }); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues.some(i => i.message.includes('schemaSource'))).toBe(true); - } - }); - - it('rejects a webSearch target with connectorId', () => { - const result = AgentCoreGatewayTargetSchema.safeParse({ - name: 'web-search', - targetType: 'webSearch', - connectorId: 'bedrock-knowledge-bases', - }); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues.some(i => i.path.includes('connectorId'))).toBe(true); - } - }); - - it('rejects a webSearch target with httpRuntime', () => { - const result = AgentCoreGatewayTargetSchema.safeParse({ - name: 'web-search', - targetType: 'webSearch', - httpRuntime: { runtime: 'my-agent' }, - }); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues.some(i => i.path.includes('httpRuntime'))).toBe(true); - } - }); - - it('rejects a webSearch target with outboundAuth', () => { - const result = AgentCoreGatewayTargetSchema.safeParse({ - name: 'web-search', - targetType: 'webSearch', - outboundAuth: { type: 'OAUTH', credentialName: 'my-cred' }, - }); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues.some(i => i.path.includes('outboundAuth'))).toBe(true); - } - }); - - it('rejects excludeDomains on a non-webSearch target', () => { + it('accepts parameterValues on non-connector types without validation', () => { const result = AgentCoreGatewayTargetSchema.safeParse({ name: 'mcp', targetType: 'mcpServer', endpoint: 'https://example.com/mcp', - excludeDomains: ['internal.example.com'], + parameterValues: { anything: true }, }); expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.issues.some(i => i.path.includes('excludeDomains'))).toBe(true); - } }); }); diff --git a/src/schema/schemas/agentcore-project.ts b/src/schema/schemas/agentcore-project.ts index b9762179c..c1eaa46fd 100644 --- a/src/schema/schemas/agentcore-project.ts +++ b/src/schema/schemas/agentcore-project.ts @@ -677,14 +677,25 @@ export const AgentCoreProjectSpecSchema = z for (const gateway of spec.agentCoreGateways ?? []) { for (const target of gateway.targets ?? []) { if (target.targetType !== 'connector') continue; - if (target.connectorId !== 'bedrock-knowledge-bases' && target.connectorId !== 'bedrock-agentic-retrieve') { + if (target.connectorId !== 'bedrock-knowledge-bases') { continue; } - if (target.knowledgeBaseId) { - validateKbReference(target, target.knowledgeBaseId, 'knowledgeBaseId'); - } - for (const value of target.knowledgeBaseIds ?? []) { - validateKbReference(target, value, 'knowledgeBaseIds[]'); + for (const cfg of target.configurations ?? []) { + const pv = cfg.parameterValues; + if (cfg.name === 'Retrieve' && pv?.knowledgeBaseId && typeof pv.knowledgeBaseId === 'string') { + validateKbReference(target, pv.knowledgeBaseId, 'configurations[].parameterValues.knowledgeBaseId'); + } + if (cfg.name === 'AgenticRetrieveStream') { + const rawRetrievers = pv?.retrievers; + if (!Array.isArray(rawRetrievers)) continue; + for (const r of rawRetrievers) { + if (!r || typeof r !== 'object') continue; + const kbId = (r as { configuration?: { knowledgeBase?: { knowledgeBaseId?: string } } }).configuration + ?.knowledgeBase?.knowledgeBaseId; + if (kbId) + validateKbReference(target, kbId, 'configurations[].parameterValues.retrievers[].knowledgeBaseId'); + } + } } } } diff --git a/src/schema/schemas/mcp.ts b/src/schema/schemas/mcp.ts index 00b86698a..aa4a289aa 100644 --- a/src/schema/schemas/mcp.ts +++ b/src/schema/schemas/mcp.ts @@ -20,7 +20,6 @@ export const GatewayTargetTypeSchema = z.enum([ 'httpRuntime', 'connector', 'passthrough', - 'webSearch', ]); export type GatewayTargetType = z.infer; @@ -51,25 +50,22 @@ export const MCP_TARGET_TYPES: readonly GatewayTargetType[] = [ * name + Enabled list via CONNECTOR_DEFAULTS. * * Spec note: the original DevEx doc (cli-knowledge-bases-devex.md) uses - * `agentic-retrieve`, but the service accepts `bedrock-agentic-retrieve`. The - * latter is canonical. + * `agentic-retrieve`, but that is now a configuration within bedrock-knowledge-bases + * rather than a standalone connector ID. */ export const CONNECTOR_ID = { BEDROCK_KNOWLEDGE_BASES: 'bedrock-knowledge-bases', - BEDROCK_AGENTIC_RETRIEVE: 'bedrock-agentic-retrieve', + WEB_SEARCH: 'web-search', } as const; -export const CONNECTOR_ID_VALUES = [ - CONNECTOR_ID.BEDROCK_KNOWLEDGE_BASES, - CONNECTOR_ID.BEDROCK_AGENTIC_RETRIEVE, -] as const; +export const CONNECTOR_ID_VALUES = [CONNECTOR_ID.BEDROCK_KNOWLEDGE_BASES, CONNECTOR_ID.WEB_SEARCH] as const; export const ConnectorIdSchema = z.enum(CONNECTOR_ID_VALUES); export type ConnectorId = z.infer; /** * Real Bedrock Knowledge Base IDs are 10 uppercase alphanumeric chars. * KB names follow the standard primitive-name shape (1-48 chars, starts with a letter). - * The two formats can never collide, so a connector target's `knowledgeBaseId` - * field is unambiguously a project KB name or a literal external KB ID. + * The two formats can never collide, so a KB reference in parameterValues + * is unambiguously a project KB name or a literal external KB ID. */ export const REAL_KB_ID_PATTERN = /^[A-Z0-9]{10}$/; @@ -124,8 +120,6 @@ export const TARGET_TYPE_AUTH_CONFIG: Record< validAuthTypes: ['GATEWAY_IAM_ROLE', 'OAUTH', 'JWT_PASSTHROUGH'], iamRoleFallback: false, }, - // Amazon Web Search is invoked via the gateway's IAM role. No outbound auth. - webSearch: { authRequired: false, validAuthTypes: [], iamRoleFallback: true }, }; // ============================================================================ @@ -471,42 +465,30 @@ export const AgentCoreGatewayTargetSchema = z */ connectorId: ConnectorIdSchema.optional(), /** - * For `bedrock-knowledge-bases` connector targets: either a project KB - * name (references an entry in `knowledgeBases[]` on the project spec) - * or a literal 10-character KB ID (refers to an external KB this project - * does not own). The L3 disambiguates by regex match. Mutually exclusive - * with `knowledgeBaseIds`. - */ - knowledgeBaseId: z - .string() - .min(1) - .max(48) - .regex(/^[a-zA-Z0-9_-]+$/, 'Must be a KB name (1-48 chars, letters/digits/dash/underscore) or a 10-char KB ID') - .optional(), - /** - * For `bedrock-agentic-retrieve` connector targets only. List of project - * KB names or literal 10-char external KB IDs that this orchestrated - * retriever should fan out across. Each entry is disambiguated the same - * way `knowledgeBaseId` is. Mutually exclusive with `knowledgeBaseId`. + * Per-operation configuration entries for connector targets. Each entry + * represents a tool the connector exposes (e.g. "Retrieve", "AgenticRetrieveStream", + * "WebSearch"). Written by the CLI translator from bespoke flags or hand-edited + * by customers for advanced settings. */ - knowledgeBaseIds: z + configurations: z .array( - z - .string() - .min(1) - .max(48) - .regex(/^[a-zA-Z0-9_-]+$/, 'Each entry must be a KB name (1-48 chars) or a 10-char KB ID') + z.object({ + name: z.string(), + parameterValues: z.record(z.string(), z.unknown()).optional(), + parameterOverrides: z + .array( + z.object({ + path: z.string(), + description: z.string().optional(), + visible: z.boolean().optional(), + }) + ) + .optional(), + }) ) - .min(1) .optional(), /** Passthrough configuration. Required for passthrough target type. */ passthrough: PassthroughConfigSchema.optional(), - /** - * For `webSearch` target type only. Domains to exclude from web search - * results. Maps to the connector's `domainFilter.exclude` parameterValue - * at synth time. - */ - excludeDomains: z.array(z.string().min(1)).min(1).optional(), }) .strict() .superRefine((data, ctx) => { @@ -768,45 +750,6 @@ export const AgentCoreGatewayTargetSchema = z path: ['connectorId'], }); } - if (data.connectorId === CONNECTOR_ID.BEDROCK_KNOWLEDGE_BASES) { - if (!data.knowledgeBaseId) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `knowledgeBaseId is required for connectorId '${data.connectorId}'`, - path: ['knowledgeBaseId'], - }); - } - if (data.knowledgeBaseIds) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `knowledgeBaseIds is not applicable for connectorId '${data.connectorId}' (use knowledgeBaseId)`, - path: ['knowledgeBaseIds'], - }); - } - } - if (data.connectorId === CONNECTOR_ID.BEDROCK_AGENTIC_RETRIEVE) { - if (!data.knowledgeBaseIds || data.knowledgeBaseIds.length === 0) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `knowledgeBaseIds (non-empty) is required for connectorId '${data.connectorId}'`, - path: ['knowledgeBaseIds'], - }); - } - if (data.knowledgeBaseId) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `knowledgeBaseId is not applicable for connectorId '${data.connectorId}' (use knowledgeBaseIds)`, - path: ['knowledgeBaseId'], - }); - } - } - if (data.excludeDomains) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `excludeDomains only applies to webSearch target type`, - path: ['excludeDomains'], - }); - } if (data.compute) { ctx.addIssue({ code: z.ZodIssueCode.custom, @@ -849,96 +792,22 @@ export const AgentCoreGatewayTargetSchema = z path: ['schemaSource'], }); } - } - if (data.targetType === 'webSearch') { - // Web search is invoked via the gateway's IAM role and takes only an - // optional excludeDomains list. Reject anything else. - if (data.compute) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: 'compute is not applicable for webSearch target type', - path: ['compute'], - }); - } - if (data.endpoint) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: 'endpoint is not applicable for webSearch target type', - path: ['endpoint'], - }); - } - if (data.toolDefinitions && data.toolDefinitions.length > 0) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: 'toolDefinitions is not applicable for webSearch target type', - path: ['toolDefinitions'], - }); - } - if (data.apiGateway) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: 'apiGateway is not applicable for webSearch target type', - path: ['apiGateway'], - }); - } - if (data.lambdaFunctionArn) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: 'lambdaFunctionArn is not applicable for webSearch target type', - path: ['lambdaFunctionArn'], - }); - } - if (data.schemaSource) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: 'schemaSource is not applicable for webSearch target type', - path: ['schemaSource'], - }); - } if (data.httpRuntime) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: 'httpRuntime is not applicable for webSearch target type', + message: 'httpRuntime is not applicable for connector target type', path: ['httpRuntime'], }); } if (data.passthrough) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: 'passthrough is not applicable for webSearch target type', + message: 'passthrough is not applicable for connector target type', path: ['passthrough'], }); } - if (data.outboundAuth && data.outboundAuth.type !== 'NONE') { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: 'outboundAuth is not applicable for webSearch target type (uses gateway IAM role)', - path: ['outboundAuth'], - }); - } - if (data.connectorId) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: 'connectorId is not applicable for webSearch target type', - path: ['connectorId'], - }); - } - if (data.knowledgeBaseId) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: 'knowledgeBaseId is not applicable for webSearch target type', - path: ['knowledgeBaseId'], - }); - } - if (data.knowledgeBaseIds) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: 'knowledgeBaseIds is not applicable for webSearch target type', - path: ['knowledgeBaseIds'], - }); - } } - if (data.targetType !== 'connector' && data.targetType !== 'webSearch') { + if (data.targetType !== 'connector') { if (data.connectorId) { ctx.addIssue({ code: z.ZodIssueCode.custom, @@ -946,25 +815,11 @@ export const AgentCoreGatewayTargetSchema = z path: ['connectorId'], }); } - if (data.knowledgeBaseId) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `knowledgeBaseId only applies to connector target type`, - path: ['knowledgeBaseId'], - }); - } - if (data.knowledgeBaseIds) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `knowledgeBaseIds only applies to connector target type`, - path: ['knowledgeBaseIds'], - }); - } - if (data.excludeDomains) { + if (data.configurations && data.configurations.length > 0) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: `excludeDomains only applies to webSearch target type`, - path: ['excludeDomains'], + message: `configurations only applies to connector target type`, + path: ['configurations'], }); } }