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, + }); } }