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