From 678de14542a12d348c7fb33e3cc85cabdd12aae3 Mon Sep 17 00:00:00 2001 From: Tejas Kashinath Date: Wed, 25 Feb 2026 14:18:41 -0500 Subject: [PATCH] feat: add VPC network mode to TUI wizard and warnings Adds VPC network mode configuration to the TUI wizard for both Create (template) and BYO (bring your own code) paths. Users can select PUBLIC or VPC mode, and when VPC is selected, provide subnet IDs and security group IDs with validation. Also adds VPC warning banners to dev and invoke TUI screens when the selected agent uses VPC network mode, alerting users about potential network behavior differences. --- src/cli/tui/hooks/useDevServer.ts | 1 + src/cli/tui/screens/agent/AddAgentScreen.tsx | 143 ++++++++++++++++-- src/cli/tui/screens/agent/types.ts | 26 +++- src/cli/tui/screens/agent/useAddAgent.ts | 14 +- src/cli/tui/screens/create/useCreateFlow.ts | 3 + src/cli/tui/screens/dev/DevScreen.tsx | 7 + .../tui/screens/generate/GenerateWizardUI.tsx | 67 +++++++- src/cli/tui/screens/generate/index.ts | 3 +- src/cli/tui/screens/generate/types.ts | 11 ++ .../tui/screens/generate/useGenerateWizard.ts | 41 ++++- .../tui/screens/generate/vpc-validation.ts | 52 +++++++ src/cli/tui/screens/invoke/InvokeScreen.tsx | 5 + src/cli/tui/screens/invoke/useInvokeFlow.ts | 10 +- 13 files changed, 360 insertions(+), 23 deletions(-) create mode 100644 src/cli/tui/screens/generate/vpc-validation.ts diff --git a/src/cli/tui/hooks/useDevServer.ts b/src/cli/tui/hooks/useDevServer.ts index 9506cf9a..cb4417fc 100644 --- a/src/cli/tui/hooks/useDevServer.ts +++ b/src/cli/tui/hooks/useDevServer.ts @@ -288,6 +288,7 @@ export function useDevServer(options: { workingDir: string; port: number; agentN stop, logFilePath: loggerRef.current?.getRelativeLogPath(), hasMemory: (project?.memories?.length ?? 0) > 0, + hasVpc: project?.agents.find(a => a.name === config?.agentName)?.networkMode === 'VPC', modelProvider: project?.agents.find(a => a.name === config?.agentName)?.modelProvider, }; } diff --git a/src/cli/tui/screens/agent/AddAgentScreen.tsx b/src/cli/tui/screens/agent/AddAgentScreen.tsx index ec936362..5353ec0d 100644 --- a/src/cli/tui/screens/agent/AddAgentScreen.tsx +++ b/src/cli/tui/screens/agent/AddAgentScreen.tsx @@ -1,5 +1,5 @@ import { APP_DIR, ConfigIO } from '../../../../lib'; -import type { ModelProvider } from '../../../../schema'; +import type { ModelProvider, NetworkMode } from '../../../../schema'; import { AgentNameSchema, DEFAULT_MODEL_IDS } from '../../../../schema'; import { computeDefaultCredentialEnvVarName } from '../../../operations/identity/create-identity'; import { @@ -16,7 +16,16 @@ import type { SelectableItem } from '../../components'; import { HELP_TEXT } from '../../constants'; import { useListNavigation, useProject } from '../../hooks'; import { generateUniqueName } from '../../utils'; -import { BUILD_TYPE_OPTIONS, GenerateWizardUI, getWizardHelpText, useGenerateWizard } from '../generate'; +import { + BUILD_TYPE_OPTIONS, + GenerateWizardUI, + NETWORK_MODE_OPTIONS, + getWizardHelpText, + parseCommaSeparatedIds, + useGenerateWizard, + validateSecurityGroupsInput, + validateSubnetsInput, +} from '../generate'; import type { BuildType } from '../generate'; import type { AddAgentConfig, AgentType } from './types'; import { @@ -52,10 +61,27 @@ interface AddAgentScreenProps { // Steps for the initial phase (before branching to create or byo) type InitialStep = 'name' | 'agentType'; // Steps for BYO path only (no framework/language - user's code already has these baked in) -type ByoStep = 'codeLocation' | 'buildType' | 'modelProvider' | 'apiKey' | 'confirm'; +type ByoStep = + | 'codeLocation' + | 'buildType' + | 'modelProvider' + | 'apiKey' + | 'networkMode' + | 'subnets' + | 'securityGroups' + | 'confirm'; const INITIAL_STEPS: InitialStep[] = ['name', 'agentType']; -const BYO_STEPS: ByoStep[] = ['codeLocation', 'buildType', 'modelProvider', 'apiKey', 'confirm']; +const BYO_STEPS: ByoStep[] = [ + 'codeLocation', + 'buildType', + 'modelProvider', + 'apiKey', + 'networkMode', + 'subnets', + 'securityGroups', + 'confirm', +]; export function AddAgentScreen({ existingAgentNames, onComplete, onExit }: AddAgentScreenProps) { // Phase 1: name + agentType selection @@ -75,6 +101,9 @@ export function AddAgentScreen({ existingAgentNames, onComplete, onExit }: AddAg buildType: 'CodeZip' as BuildType, modelProvider: 'Bedrock' as ModelProvider, apiKey: undefined as string | undefined, + networkMode: 'PUBLIC' as NetworkMode, + subnets: undefined as string[] | undefined, + securityGroups: undefined as string[] | undefined, }); const { project } = useProject(); @@ -156,6 +185,9 @@ export function AddAgentScreen({ existingAgentNames, onComplete, onExit }: AddAg apiKey: generateWizard.config.apiKey, pythonVersion: DEFAULT_PYTHON_VERSION, memory: generateWizard.config.memory, + networkMode: generateWizard.config.networkMode ?? 'PUBLIC', + subnets: generateWizard.config.subnets, + securityGroups: generateWizard.config.securityGroups, }; onComplete(config); }, [name, generateWizard.config, onComplete]); @@ -174,13 +206,17 @@ export function AddAgentScreen({ existingAgentNames, onComplete, onExit }: AddAg // BYO Path // ───────────────────────────────────────────────────────────────────────────── - // BYO steps filtering (remove apiKey for Bedrock) + // BYO steps filtering (remove apiKey for Bedrock, subnets/securityGroups when not VPC) const byoSteps = useMemo(() => { + let steps = BYO_STEPS; if (byoConfig.modelProvider === 'Bedrock') { - return BYO_STEPS.filter(s => s !== 'apiKey'); + steps = steps.filter(s => s !== 'apiKey'); + } + if (byoConfig.networkMode !== 'VPC') { + steps = steps.filter(s => s !== 'subnets' && s !== 'securityGroups'); } - return BYO_STEPS; - }, [byoConfig.modelProvider]); + return steps; + }, [byoConfig.modelProvider, byoConfig.networkMode]); const byoCurrentIndex = byoSteps.indexOf(byoStep); @@ -232,6 +268,9 @@ export function AddAgentScreen({ existingAgentNames, onComplete, onExit }: AddAg apiKey: byoConfig.apiKey, pythonVersion: DEFAULT_PYTHON_VERSION, memory: 'none', + networkMode: byoConfig.networkMode, + subnets: byoConfig.subnets, + securityGroups: byoConfig.securityGroups, }; onComplete(config); }, [name, byoConfig, onComplete]); @@ -254,13 +293,40 @@ export function AddAgentScreen({ existingAgentNames, onComplete, onExit }: AddAg if (provider !== 'Bedrock') { setByoStep('apiKey'); } else { - setByoStep('confirm'); + setByoStep('networkMode'); } }, onExit: handleByoBack, isActive: isByoPath && byoStep === 'modelProvider', }); + // BYO network mode options + const networkModeItems: SelectableItem[] = useMemo( + () => + NETWORK_MODE_OPTIONS.map(o => ({ + id: o.id, + title: o.title, + description: o.description, + })), + [] + ); + + const networkModeNav = useListNavigation({ + items: networkModeItems, + onSelect: item => { + const mode = item.id as NetworkMode; + if (mode === 'PUBLIC') { + setByoConfig(c => ({ ...c, networkMode: mode, subnets: undefined, securityGroups: undefined })); + setByoStep('confirm'); + } else { + setByoConfig(c => ({ ...c, networkMode: mode })); + setByoStep('subnets'); + } + }, + onExit: handleByoBack, + isActive: isByoPath && byoStep === 'networkMode', + }); + useListNavigation({ items: [{ id: 'confirm', title: 'Confirm' }], onSelect: handleByoComplete, @@ -281,7 +347,7 @@ export function AddAgentScreen({ existingAgentNames, onComplete, onExit }: AddAg return getWizardHelpText(generateWizard.step); } // BYO path - if (byoStep === 'codeLocation' || byoStep === 'apiKey') { + if (byoStep === 'codeLocation' || byoStep === 'apiKey' || byoStep === 'subnets' || byoStep === 'securityGroups') { return HELP_TEXT.TEXT_INPUT; } if (byoStep === 'confirm') { @@ -413,10 +479,58 @@ export function AddAgentScreen({ existingAgentNames, onComplete, onExit }: AddAg envVarName={getProviderInfo(byoConfig.modelProvider).envVarName} onSubmit={apiKey => { setByoConfig(c => ({ ...c, apiKey })); + setByoStep('networkMode'); + }} + onSkip={() => setByoStep('networkMode')} + onCancel={handleByoBack} + /> + )} + + {byoStep === 'networkMode' && ( + + )} + + {byoStep === 'subnets' && ( + + + Note: Your agent will run inside these VPC subnets. Ensure they have connectivity to required services + (S3, ECR, Bedrock) and public internet if using public MCP servers or non-Bedrock model providers. + + + { + const result = validateSubnetsInput(value); + if (result !== true) return false; + setByoConfig(c => ({ ...c, subnets: parseCommaSeparatedIds(value) })); + setByoStep('securityGroups'); + return true; + }} + onCancel={handleByoBack} + customValidation={validateSubnetsInput} + /> + + + )} + + {byoStep === 'securityGroups' && ( + { + const result = validateSecurityGroupsInput(value); + if (result !== true) return false; + setByoConfig(c => ({ ...c, securityGroups: parseCommaSeparatedIds(value) })); setByoStep('confirm'); + return true; }} - onSkip={() => setByoStep('confirm')} onCancel={handleByoBack} + customValidation={validateSecurityGroupsInput} /> )} @@ -450,6 +564,13 @@ export function AddAgentScreen({ existingAgentNames, onComplete, onExit }: AddAg }, ] : []), + { label: 'Network Mode', value: byoConfig.networkMode }, + ...(byoConfig.networkMode === 'VPC' && byoConfig.subnets + ? [{ label: 'Subnets', value: byoConfig.subnets.join(', ') }] + : []), + ...(byoConfig.networkMode === 'VPC' && byoConfig.securityGroups + ? [{ label: 'Security Groups', value: byoConfig.securityGroups.join(', ') }] + : []), ]} /> )} diff --git a/src/cli/tui/screens/agent/types.ts b/src/cli/tui/screens/agent/types.ts index 48a38d0b..5c460fa5 100644 --- a/src/cli/tui/screens/agent/types.ts +++ b/src/cli/tui/screens/agent/types.ts @@ -1,4 +1,11 @@ -import type { BuildType, ModelProvider, PythonRuntime, SDKFramework, TargetLanguage } from '../../../../schema'; +import type { + BuildType, + ModelProvider, + NetworkMode, + PythonRuntime, + SDKFramework, + TargetLanguage, +} from '../../../../schema'; import { DEFAULT_MODEL_IDS, getSupportedModelProviders } from '../../../../schema'; import type { MemoryOption } from '../generate/types'; @@ -35,6 +42,9 @@ export type AddAgentStep = | 'modelProvider' | 'apiKey' | 'memory' + | 'networkMode' + | 'subnets' + | 'securityGroups' | 'confirm'; export interface AddAgentConfig { @@ -54,6 +64,12 @@ export interface AddAgentConfig { pythonVersion: PythonRuntime; /** Memory option (create path only) */ memory: MemoryOption; + /** Network mode for the agent runtime */ + networkMode: NetworkMode; + /** VPC subnet IDs (required when networkMode is VPC) */ + subnets?: string[]; + /** VPC security group IDs (required when networkMode is VPC) */ + securityGroups?: string[]; } export const ADD_AGENT_STEP_LABELS: Record = { @@ -66,6 +82,9 @@ export const ADD_AGENT_STEP_LABELS: Record = { modelProvider: 'Model', apiKey: 'API Key', memory: 'Memory', + networkMode: 'Network', + subnets: 'Subnets', + securityGroups: 'Sec Groups', confirm: 'Confirm', }; @@ -102,6 +121,11 @@ export const MODEL_PROVIDER_OPTIONS = [ { id: 'Gemini', title: `Google Gemini (${DEFAULT_MODEL_IDS.Gemini})`, description: 'Gemini models via Google API' }, ] as const; +export const NETWORK_MODE_OPTIONS = [ + { id: 'PUBLIC', title: 'Public', description: 'Agent runs with public internet access (default)' }, + { id: 'VPC', title: 'VPC', description: 'Agent runs inside your VPC subnets' }, +] as const; + /** * Get model provider options filtered by SDK framework compatibility. */ diff --git a/src/cli/tui/screens/agent/useAddAgent.ts b/src/cli/tui/screens/agent/useAddAgent.ts index c5830385..001095a9 100644 --- a/src/cli/tui/screens/agent/useAddAgent.ts +++ b/src/cli/tui/screens/agent/useAddAgent.ts @@ -54,15 +54,22 @@ export type AddAgentOutcome = AddAgentCreateResult | AddAgentByoResult | AddAgen * Maps AddAgentConfig (from BYO wizard) to v2 AgentEnvSpec for schema persistence. */ export function mapByoConfigToAgent(config: AddAgentConfig): AgentEnvSpec { - return { + const agent: AgentEnvSpec = { type: 'AgentCoreRuntime', name: config.name, build: config.buildType, entrypoint: config.entrypoint as FilePath, codeLocation: config.codeLocation as DirectoryPath, runtimeVersion: config.pythonVersion, - networkMode: 'PUBLIC', + networkMode: config.networkMode ?? 'PUBLIC', }; + if (config.networkMode === 'VPC' && config.subnets && config.securityGroups) { + agent.networkConfig = { + subnets: config.subnets, + securityGroups: config.securityGroups, + }; + } + return agent; } /** @@ -76,6 +83,9 @@ function mapAddAgentConfigToGenerateConfig(config: AddAgentConfig): GenerateConf modelProvider: config.modelProvider, memory: config.memory, language: config.language, + networkMode: config.networkMode, + subnets: config.subnets, + securityGroups: config.securityGroups, }; } diff --git a/src/cli/tui/screens/create/useCreateFlow.ts b/src/cli/tui/screens/create/useCreateFlow.ts index 324b9e3c..4af4e60b 100644 --- a/src/cli/tui/screens/create/useCreateFlow.ts +++ b/src/cli/tui/screens/create/useCreateFlow.ts @@ -274,6 +274,9 @@ export function useCreateFlow(cwd: string): CreateFlowState { memory: addAgentConfig.memory, language: addAgentConfig.language, apiKey: addAgentConfig.apiKey, + networkMode: addAgentConfig.networkMode, + subnets: addAgentConfig.subnets, + securityGroups: addAgentConfig.securityGroups, }; logger.logSubStep(`Framework: ${generateConfig.sdk}`); diff --git a/src/cli/tui/screens/dev/DevScreen.tsx b/src/cli/tui/screens/dev/DevScreen.tsx index 619b0ca5..a2ff0a5f 100644 --- a/src/cli/tui/screens/dev/DevScreen.tsx +++ b/src/cli/tui/screens/dev/DevScreen.tsx @@ -177,6 +177,7 @@ export function DevScreen(props: DevScreenProps) { stop, logFilePath, hasMemory, + hasVpc, modelProvider, } = useDevServer({ workingDir, @@ -444,6 +445,12 @@ export function DevScreen(props: DevScreenProps) { AgentCore memory is not available when running locally. To test memory, deploy and use invoke. )} + {hasVpc && ( + + This agent uses VPC network mode. Local dev server runs outside your VPC. Network behavior may differ from + deployed environment. + + )} ); diff --git a/src/cli/tui/screens/generate/GenerateWizardUI.tsx b/src/cli/tui/screens/generate/GenerateWizardUI.tsx index 78311d7e..276b7528 100644 --- a/src/cli/tui/screens/generate/GenerateWizardUI.tsx +++ b/src/cli/tui/screens/generate/GenerateWizardUI.tsx @@ -1,4 +1,4 @@ -import type { ModelProvider } from '../../../../schema'; +import type { ModelProvider, NetworkMode } from '../../../../schema'; import { DEFAULT_MODEL_IDS, ProjectNameSchema } from '../../../../schema'; import { computeDefaultCredentialEnvVarName } from '../../../operations/identity/create-identity'; import { ApiKeySecretInput, Panel, SelectList, StepIndicator, TextInput } from '../../components'; @@ -9,11 +9,13 @@ import { BUILD_TYPE_OPTIONS, LANGUAGE_OPTIONS, MEMORY_OPTIONS, + NETWORK_MODE_OPTIONS, SDK_OPTIONS, STEP_LABELS, getModelProviderOptionsForSdk, } from './types'; import type { useGenerateWizard } from './useGenerateWizard'; +import { parseCommaSeparatedIds, validateSecurityGroupsInput, validateSubnetsInput } from './vpc-validation'; import { Box, Text, useInput } from 'ink'; // Helper to get provider display name and env var name from ModelProvider @@ -70,6 +72,8 @@ export function GenerateWizardUI({ })); case 'memory': return MEMORY_OPTIONS.map(o => ({ id: o.id, title: o.title, description: o.description })); + case 'networkMode': + return NETWORK_MODE_OPTIONS.map(o => ({ id: o.id, title: o.title, description: o.description })); default: return []; } @@ -79,6 +83,8 @@ export function GenerateWizardUI({ const isSelectStep = items.length > 0; const isTextStep = wizard.step === 'projectName'; const isApiKeyStep = wizard.step === 'apiKey'; + const isSubnetsStep = wizard.step === 'subnets'; + const isSecurityGroupsStep = wizard.step === 'securityGroups'; const isConfirmStep = wizard.step === 'confirm'; const handleSelect = (item: SelectableItem) => { @@ -98,6 +104,9 @@ export function GenerateWizardUI({ case 'memory': wizard.setMemory(item.id as MemoryOption); break; + case 'networkMode': + wizard.setNetworkMode(item.id as NetworkMode); + break; } }; @@ -154,6 +163,44 @@ export function GenerateWizardUI({ /> )} + {isSubnetsStep && ( + + + Note: Your agent will run inside these VPC subnets. Ensure they have connectivity to required services (S3, + ECR, Bedrock) and public internet if using public MCP servers or non-Bedrock model providers. + + + { + const result = validateSubnetsInput(value); + if (result !== true) return false; + wizard.setSubnets(parseCommaSeparatedIds(value)); + return true; + }} + onCancel={onBack} + customValidation={validateSubnetsInput} + /> + + + )} + + {isSecurityGroupsStep && ( + { + const result = validateSecurityGroupsInput(value); + if (result !== true) return false; + wizard.setSecurityGroups(parseCommaSeparatedIds(value)); + return true; + }} + onCancel={onBack} + customValidation={validateSecurityGroupsInput} + /> + )} + {isConfirmStep && } ); @@ -165,7 +212,7 @@ export function GenerateWizardUI({ // eslint-disable-next-line react-refresh/only-export-components export function getWizardHelpText(step: GenerateStep): string { if (step === 'confirm') return 'Enter/Y confirm · Esc back'; - if (step === 'projectName') return 'Enter submit · Esc cancel'; + if (step === 'projectName' || step === 'subnets' || step === 'securityGroups') return 'Enter submit · Esc cancel'; if (step === 'apiKey') return 'Enter submit · Tab show/hide · Esc back'; return '↑↓ navigate · Enter select · Esc back'; } @@ -236,6 +283,22 @@ function ConfirmView({ config, credentialProjectName }: { config: GenerateConfig Memory: {memoryLabel} + + Network Mode: + {config.networkMode ?? 'PUBLIC'} + + {config.networkMode === 'VPC' && config.subnets && ( + + Subnets: + {config.subnets.join(', ')} + + )} + {config.networkMode === 'VPC' && config.securityGroups && ( + + Security Groups: + {config.securityGroups.join(', ')} + + )} ); diff --git a/src/cli/tui/screens/generate/index.ts b/src/cli/tui/screens/generate/index.ts index 29bc5d8c..2015d75d 100644 --- a/src/cli/tui/screens/generate/index.ts +++ b/src/cli/tui/screens/generate/index.ts @@ -3,4 +3,5 @@ export { useGenerateWizard } from './useGenerateWizard'; export type { UseGenerateWizardOptions } from './useGenerateWizard'; export { GenerateWizardUI, GenerateWizardStepIndicator, getWizardHelpText } from './GenerateWizardUI'; export type { BuildType, GenerateConfig, GenerateStep, MemoryOption } from './types'; -export { BUILD_TYPE_OPTIONS } from './types'; +export { BUILD_TYPE_OPTIONS, NETWORK_MODE_OPTIONS } from './types'; +export { parseCommaSeparatedIds, validateSubnetsInput, validateSecurityGroupsInput } from './vpc-validation'; diff --git a/src/cli/tui/screens/generate/types.ts b/src/cli/tui/screens/generate/types.ts index 3f9e6836..2211e732 100644 --- a/src/cli/tui/screens/generate/types.ts +++ b/src/cli/tui/screens/generate/types.ts @@ -9,6 +9,9 @@ export type GenerateStep = | 'modelProvider' | 'apiKey' | 'memory' + | 'networkMode' + | 'subnets' + | 'securityGroups' | 'confirm'; export type MemoryOption = 'none' | 'shortTerm' | 'longAndShortTerm'; @@ -49,6 +52,9 @@ export const STEP_LABELS: Record = { modelProvider: 'Model', apiKey: 'API Key', memory: 'Memory', + networkMode: 'Network', + subnets: 'Subnets', + securityGroups: 'Sec Groups', confirm: 'Confirm', }; @@ -88,6 +94,11 @@ export function getModelProviderOptionsForSdk(sdk: SDKFramework) { return MODEL_PROVIDER_OPTIONS.filter(option => supportedProviders.includes(option.id)); } +export const NETWORK_MODE_OPTIONS = [ + { id: 'PUBLIC', title: 'Public', description: 'Agent runs with public internet access (default)' }, + { id: 'VPC', title: 'VPC', description: 'Agent runs inside your VPC subnets' }, +] as const; + export const MEMORY_OPTIONS = [ { id: 'none', title: 'None', description: 'No memory' }, { id: 'shortTerm', title: 'Short-term memory', description: 'Context within a session' }, diff --git a/src/cli/tui/screens/generate/useGenerateWizard.ts b/src/cli/tui/screens/generate/useGenerateWizard.ts index 9f5e653c..de11539c 100644 --- a/src/cli/tui/screens/generate/useGenerateWizard.ts +++ b/src/cli/tui/screens/generate/useGenerateWizard.ts @@ -1,3 +1,4 @@ +import type { NetworkMode } from '../../../../schema'; import { ProjectNameSchema } from '../../../../schema'; import type { BuildType, GenerateConfig, GenerateStep, MemoryOption } from './types'; import { BASE_GENERATE_STEPS, getModelProviderOptionsForSdk } from './types'; @@ -11,6 +12,7 @@ function getDefaultConfig(): GenerateConfig { modelProvider: 'Bedrock', memory: 'none', language: 'Python', + networkMode: 'PUBLIC', }; } @@ -48,8 +50,16 @@ export function useGenerateWizard(options?: UseGenerateWizardOptions) { const confirmIndex = filtered.indexOf('confirm'); filtered = [...filtered.slice(0, confirmIndex), 'memory', ...filtered.slice(confirmIndex)]; } + // Insert networkMode before confirm + const confirmIndex = filtered.indexOf('confirm'); + filtered = [...filtered.slice(0, confirmIndex), 'networkMode', ...filtered.slice(confirmIndex)]; + // Add subnets and securityGroups after networkMode when VPC is selected + if (config.networkMode === 'VPC') { + const nmIndex = filtered.indexOf('networkMode'); + filtered = [...filtered.slice(0, nmIndex + 1), 'subnets', 'securityGroups', ...filtered.slice(nmIndex + 1)]; + } return filtered; - }, [config.modelProvider, config.sdk, hasInitialName, sdkSelected]); + }, [config.modelProvider, config.sdk, config.networkMode, hasInitialName, sdkSelected]); const currentIndex = steps.indexOf(step); @@ -98,7 +108,7 @@ export function useGenerateWizard(options?: UseGenerateWizardOptions) { } else if (config.sdk === 'Strands') { setStep('memory'); } else { - setStep('confirm'); + setStep('networkMode'); } }, [config.sdk] @@ -110,7 +120,7 @@ export function useGenerateWizard(options?: UseGenerateWizardOptions) { if (config.sdk === 'Strands') { setStep('memory'); } else { - setStep('confirm'); + setStep('networkMode'); } }, [config.sdk] @@ -120,12 +130,32 @@ export function useGenerateWizard(options?: UseGenerateWizardOptions) { if (config.sdk === 'Strands') { setStep('memory'); } else { - setStep('confirm'); + setStep('networkMode'); } }, [config.sdk]); const setMemory = useCallback((memory: MemoryOption) => { setConfig(c => ({ ...c, memory })); + setStep('networkMode'); + }, []); + + const setNetworkMode = useCallback((networkMode: NetworkMode) => { + if (networkMode === 'PUBLIC') { + setConfig(c => ({ ...c, networkMode, subnets: undefined, securityGroups: undefined })); + setStep('confirm'); + } else { + setConfig(c => ({ ...c, networkMode })); + setStep('subnets'); + } + }, []); + + const setSubnets = useCallback((subnets: string[]) => { + setConfig(c => ({ ...c, subnets })); + setStep('securityGroups'); + }, []); + + const setSecurityGroups = useCallback((securityGroups: string[]) => { + setConfig(c => ({ ...c, securityGroups })); setStep('confirm'); }, []); @@ -168,6 +198,9 @@ export function useGenerateWizard(options?: UseGenerateWizardOptions) { setApiKey, skipApiKey, setMemory, + setNetworkMode, + setSubnets, + setSecurityGroups, goBack, reset, initWithName, diff --git a/src/cli/tui/screens/generate/vpc-validation.ts b/src/cli/tui/screens/generate/vpc-validation.ts new file mode 100644 index 00000000..275d5b57 --- /dev/null +++ b/src/cli/tui/screens/generate/vpc-validation.ts @@ -0,0 +1,52 @@ +const SUBNET_REGEX = /^subnet-[0-9a-zA-Z]{8,17}$/; +const SECURITY_GROUP_REGEX = /^sg-[0-9a-zA-Z]{8,17}$/; + +/** + * Parse a comma-separated string of IDs into a trimmed array. + */ +export function parseCommaSeparatedIds(value: string): string[] { + return value + .split(',') + .map(s => s.trim()) + .filter(s => s.length > 0); +} + +/** + * Validate comma-separated subnet IDs. + * Returns `true` if valid, or an error message string if invalid. + */ +export function validateSubnetsInput(value: string): true | string { + const ids = parseCommaSeparatedIds(value); + if (ids.length === 0) { + return 'At least one subnet ID is required'; + } + if (ids.length > 16) { + return 'Maximum 16 subnet IDs allowed'; + } + for (const id of ids) { + if (!SUBNET_REGEX.test(id)) { + return `Invalid subnet ID: "${id}". Expected format: subnet-xxxxxxxx`; + } + } + return true; +} + +/** + * Validate comma-separated security group IDs. + * Returns `true` if valid, or an error message string if invalid. + */ +export function validateSecurityGroupsInput(value: string): true | string { + const ids = parseCommaSeparatedIds(value); + if (ids.length === 0) { + return 'At least one security group ID is required'; + } + if (ids.length > 16) { + return 'Maximum 16 security group IDs allowed'; + } + for (const id of ids) { + if (!SECURITY_GROUP_REGEX.test(id)) { + return `Invalid security group ID: "${id}". Expected format: sg-xxxxxxxx`; + } + } + return true; +} diff --git a/src/cli/tui/screens/invoke/InvokeScreen.tsx b/src/cli/tui/screens/invoke/InvokeScreen.tsx index b1f1accb..c1c9d5de 100644 --- a/src/cli/tui/screens/invoke/InvokeScreen.tsx +++ b/src/cli/tui/screens/invoke/InvokeScreen.tsx @@ -321,6 +321,11 @@ export function InvokeScreen({ )} {logFilePath && } + {mode !== 'select-agent' && agent?.networkMode === 'VPC' && ( + + This agent uses VPC network mode. Network behavior may differ if VPC endpoints are not configured. + + )} ); diff --git a/src/cli/tui/screens/invoke/useInvokeFlow.ts b/src/cli/tui/screens/invoke/useInvokeFlow.ts index 5fdae047..97950fc8 100644 --- a/src/cli/tui/screens/invoke/useInvokeFlow.ts +++ b/src/cli/tui/screens/invoke/useInvokeFlow.ts @@ -3,6 +3,7 @@ import type { AgentCoreDeployedState, AwsDeploymentTarget, ModelProvider, + NetworkMode, AgentCoreProjectSpec as _AgentCoreProjectSpec, } from '../../../../schema'; import { DEFAULT_RUNTIME_USER_ID, invokeAgentRuntimeStreaming } from '../../../aws'; @@ -12,7 +13,7 @@ import { generateSessionId } from '../../../operations/session'; import { useCallback, useEffect, useRef, useState } from 'react'; export interface InvokeConfig { - agents: { name: string; state: AgentCoreDeployedState; modelProvider?: ModelProvider }[]; + agents: { name: string; state: AgentCoreDeployedState; modelProvider?: ModelProvider; networkMode?: NetworkMode }[]; target: AwsDeploymentTarget; targetName: string; projectName: string; @@ -82,7 +83,12 @@ export function useInvokeFlow(options: InvokeFlowOptions = {}): InvokeFlowState for (const agent of project.agents) { const state = targetState?.resources?.agents?.[agent.name]; if (state) { - agents.push({ name: agent.name, state, modelProvider: agent.modelProvider }); + agents.push({ + name: agent.name, + state, + modelProvider: agent.modelProvider, + networkMode: agent.networkMode, + }); } }