Skip to content

Commit eb8aee7

Browse files
committed
feat: add VPC network mode support for agent runtimes
Add support for deploying AgentCore Runtimes in VPC network mode, enabling agents to run within customer VPCs with specified subnets and security groups. Changes: - Fix NetworkModeSchema enum: PRIVATE → VPC to match AWS API - Add NetworkConfigSchema with subnet/security group validation - Add networkConfig field to AgentEnvSpec with cross-field validation - Add --network-mode, --subnets, --security-groups CLI flags to create and add commands - Extract shared VPC validation and parsing utilities - Add VPC info messages to dev and invoke commands - Update LLM-compacted schema documentation - Add comprehensive unit tests for VPC validation
1 parent 0b39e45 commit eb8aee7

File tree

23 files changed

+473
-31
lines changed

23 files changed

+473
-31
lines changed

src/cli/commands/add/__tests__/validate.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,52 @@ describe('validate', () => {
162162
expect(validateAddAgentOptions(validAgentOptionsByo)).toEqual({ valid: true });
163163
expect(validateAddAgentOptions(validAgentOptionsCreate)).toEqual({ valid: true });
164164
});
165+
166+
// VPC validation tests
167+
it('rejects invalid network mode', () => {
168+
const result = validateAddAgentOptions({ ...validAgentOptionsCreate, networkMode: 'INVALID' as any });
169+
expect(result.valid).toBe(false);
170+
expect(result.error).toContain('Invalid network mode');
171+
});
172+
173+
it('rejects VPC mode without subnets', () => {
174+
const result = validateAddAgentOptions({
175+
...validAgentOptionsCreate,
176+
networkMode: 'VPC',
177+
securityGroups: 'sg-12345678',
178+
});
179+
expect(result.valid).toBe(false);
180+
expect(result.error).toContain('--subnets is required');
181+
});
182+
183+
it('rejects VPC mode without security groups', () => {
184+
const result = validateAddAgentOptions({
185+
...validAgentOptionsCreate,
186+
networkMode: 'VPC',
187+
subnets: 'subnet-12345678',
188+
});
189+
expect(result.valid).toBe(false);
190+
expect(result.error).toContain('--security-groups is required');
191+
});
192+
193+
it('rejects subnets without VPC mode', () => {
194+
const result = validateAddAgentOptions({
195+
...validAgentOptionsCreate,
196+
subnets: 'subnet-12345678',
197+
});
198+
expect(result.valid).toBe(false);
199+
expect(result.error).toContain('require --network-mode VPC');
200+
});
201+
202+
it('passes for valid VPC options', () => {
203+
const result = validateAddAgentOptions({
204+
...validAgentOptionsCreate,
205+
networkMode: 'VPC',
206+
subnets: 'subnet-12345678',
207+
securityGroups: 'sg-12345678',
208+
});
209+
expect(result.valid).toBe(true);
210+
});
165211
});
166212

167213
describe('validateAddGatewayOptions', () => {

src/cli/commands/add/actions.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
GatewayAuthorizerType,
88
MemoryStrategyType,
99
ModelProvider,
10+
NetworkMode,
1011
SDKFramework,
1112
TargetLanguage,
1213
} from '../../../schema';
@@ -29,6 +30,7 @@ import { createRenderer } from '../../templates';
2930
import type { MemoryOption } from '../../tui/screens/generate/types';
3031
import type { AddGatewayConfig, AddMcpToolConfig } from '../../tui/screens/mcp/types';
3132
import { DEFAULT_EVENT_EXPIRY } from '../../tui/screens/memory/types';
33+
import { parseCommaSeparatedList } from '../shared/vpc-utils';
3234
import type { AddAgentResult, AddGatewayResult, AddIdentityResult, AddMcpToolResult, AddMemoryResult } from './types';
3335
import { mkdirSync } from 'fs';
3436
import { dirname, join } from 'path';
@@ -43,6 +45,9 @@ export interface ValidatedAddAgentOptions {
4345
modelProvider: ModelProvider;
4446
apiKey?: string;
4547
memory?: MemoryOption;
48+
networkMode?: NetworkMode;
49+
subnets?: string;
50+
securityGroups?: string;
4651
codeLocation?: string;
4752
entrypoint?: string;
4853
}
@@ -120,6 +125,9 @@ async function handleCreatePath(options: ValidatedAddAgentOptions, configBaseDir
120125
modelProvider: options.modelProvider,
121126
memory: options.memory!,
122127
language: options.language,
128+
networkMode: options.networkMode,
129+
subnets: parseCommaSeparatedList(options.subnets),
130+
securityGroups: parseCommaSeparatedList(options.securityGroups),
123131
};
124132

125133
const agentPath = join(projectRoot, APP_DIR, options.name);
@@ -186,14 +194,23 @@ async function handleByoPath(
186194

187195
const project = await configIO.readProjectSpec();
188196

197+
const networkMode = options.networkMode ?? 'PUBLIC';
189198
const agent: AgentEnvSpec = {
190199
type: 'AgentCoreRuntime',
191200
name: options.name,
192201
build: options.buildType,
193202
entrypoint: (options.entrypoint ?? 'main.py') as FilePath,
194203
codeLocation: codeLocation as DirectoryPath,
195204
runtimeVersion: 'PYTHON_3_12',
196-
networkMode: 'PUBLIC',
205+
networkMode,
206+
...(networkMode === 'VPC' && options.subnets && options.securityGroups
207+
? {
208+
networkConfig: {
209+
subnets: parseCommaSeparatedList(options.subnets)!,
210+
securityGroups: parseCommaSeparatedList(options.securityGroups)!,
211+
},
212+
}
213+
: {}),
197214
};
198215

199216
project.agents.push(agent);

src/cli/commands/add/command.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ async function handleAddAgentCLI(options: AddAgentOptions): Promise<void> {
4040
modelProvider: options.modelProvider!,
4141
apiKey: options.apiKey,
4242
memory: options.memory,
43+
networkMode: options.networkMode,
44+
subnets: options.subnets,
45+
securityGroups: options.securityGroups,
4346
codeLocation: options.codeLocation,
4447
entrypoint: options.entrypoint,
4548
});
@@ -227,6 +230,9 @@ export function registerAdd(program: Command) {
227230
.option('--model-provider <provider>', 'Model provider: Bedrock, Anthropic, OpenAI, Gemini [non-interactive]')
228231
.option('--api-key <key>', 'API key for non-Bedrock providers [non-interactive]')
229232
.option('--memory <mem>', 'Memory: none, shortTerm, longAndShortTerm (create path only) [non-interactive]')
233+
.option('--network-mode <mode>', 'Network mode: PUBLIC or VPC (default: PUBLIC) [non-interactive]')
234+
.option('--subnets <ids>', 'Comma-separated subnet IDs (required for VPC mode) [non-interactive]')
235+
.option('--security-groups <ids>', 'Comma-separated security group IDs (required for VPC mode) [non-interactive]')
230236
.option('--code-location <path>', 'Path to existing code (BYO path only) [non-interactive]')
231237
.option('--entrypoint <file>', 'Entry file relative to code-location (BYO, default: main.py) [non-interactive]')
232238
.option('--json', 'Output as JSON [non-interactive]')

src/cli/commands/add/types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { GatewayAuthorizerType, ModelProvider, SDKFramework, TargetLanguage } from '../../../schema';
1+
import type { GatewayAuthorizerType, ModelProvider, NetworkMode, SDKFramework, TargetLanguage } from '../../../schema';
22
import type { MemoryOption } from '../../tui/screens/generate/types';
33

44
// Agent types
@@ -11,6 +11,9 @@ export interface AddAgentOptions {
1111
modelProvider?: ModelProvider;
1212
apiKey?: string;
1313
memory?: MemoryOption;
14+
networkMode?: NetworkMode;
15+
subnets?: string;
16+
securityGroups?: string;
1417
codeLocation?: string;
1518
entrypoint?: string;
1619
json?: boolean;

src/cli/commands/add/validate.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
TargetLanguageSchema,
88
getSupportedModelProviders,
99
} from '../../../schema';
10+
import { validateVpcOptions } from '../shared/vpc-utils';
1011
import type {
1112
AddAgentOptions,
1213
AddGatewayOptions,
@@ -102,6 +103,9 @@ export function validateAddAgentOptions(options: AddAgentOptions): ValidationRes
102103
}
103104
}
104105

106+
const vpcResult = validateVpcOptions(options);
107+
if (!vpcResult.valid) return vpcResult;
108+
105109
return { valid: true };
106110
}
107111

src/cli/commands/create/__tests__/validate.test.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,90 @@ describe('validateCreateOptions', () => {
132132
expect(result.valid).toBe(true);
133133
});
134134

135+
// VPC validation tests
136+
it('rejects invalid network mode', () => {
137+
const result = validateCreateOptions(
138+
{
139+
name: 'VpcTest1',
140+
language: 'Python',
141+
framework: 'Strands',
142+
modelProvider: 'Bedrock',
143+
memory: 'none',
144+
networkMode: 'INVALID',
145+
},
146+
testDir
147+
);
148+
expect(result.valid).toBe(false);
149+
expect(result.error).toContain('Invalid network mode');
150+
});
151+
152+
it('rejects VPC mode without subnets', () => {
153+
const result = validateCreateOptions(
154+
{
155+
name: 'VpcTest2',
156+
language: 'Python',
157+
framework: 'Strands',
158+
modelProvider: 'Bedrock',
159+
memory: 'none',
160+
networkMode: 'VPC',
161+
securityGroups: 'sg-12345678',
162+
},
163+
testDir
164+
);
165+
expect(result.valid).toBe(false);
166+
expect(result.error).toContain('--subnets is required');
167+
});
168+
169+
it('rejects VPC mode without security groups', () => {
170+
const result = validateCreateOptions(
171+
{
172+
name: 'VpcTest3',
173+
language: 'Python',
174+
framework: 'Strands',
175+
modelProvider: 'Bedrock',
176+
memory: 'none',
177+
networkMode: 'VPC',
178+
subnets: 'subnet-12345678',
179+
},
180+
testDir
181+
);
182+
expect(result.valid).toBe(false);
183+
expect(result.error).toContain('--security-groups is required');
184+
});
185+
186+
it('rejects subnets without VPC mode', () => {
187+
const result = validateCreateOptions(
188+
{
189+
name: 'VpcTest4',
190+
language: 'Python',
191+
framework: 'Strands',
192+
modelProvider: 'Bedrock',
193+
memory: 'none',
194+
subnets: 'subnet-12345678',
195+
},
196+
testDir
197+
);
198+
expect(result.valid).toBe(false);
199+
expect(result.error).toContain('require --network-mode VPC');
200+
});
201+
202+
it('returns valid with VPC mode and required options', () => {
203+
const result = validateCreateOptions(
204+
{
205+
name: 'VpcTest5',
206+
language: 'Python',
207+
framework: 'Strands',
208+
modelProvider: 'Bedrock',
209+
memory: 'none',
210+
networkMode: 'VPC',
211+
subnets: 'subnet-12345678',
212+
securityGroups: 'sg-12345678',
213+
},
214+
testDir
215+
);
216+
expect(result.valid).toBe(true);
217+
});
218+
135219
it('returns invalid for unsupported framework/model combination', () => {
136220
// GoogleADK only supports certain providers, not all
137221
const result = validateCreateOptions(

src/cli/commands/create/action.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
BuildType,
55
DeployedState,
66
ModelProvider,
7+
NetworkMode,
78
SDKFramework,
89
TargetLanguage,
910
} from '../../../schema';
@@ -120,6 +121,9 @@ export interface CreateWithAgentOptions {
120121
modelProvider: ModelProvider;
121122
apiKey?: string;
122123
memory: MemoryOption;
124+
networkMode?: NetworkMode;
125+
subnets?: string[];
126+
securityGroups?: string[];
123127
skipGit?: boolean;
124128
skipPythonSetup?: boolean;
125129
onProgress?: ProgressCallback;
@@ -135,6 +139,9 @@ export async function createProjectWithAgent(options: CreateWithAgentOptions): P
135139
modelProvider,
136140
apiKey,
137141
memory,
142+
networkMode,
143+
subnets,
144+
securityGroups,
138145
skipGit,
139146
skipPythonSetup,
140147
onProgress,
@@ -172,6 +179,9 @@ export async function createProjectWithAgent(options: CreateWithAgentOptions): P
172179
apiKey,
173180
memory,
174181
language,
182+
networkMode,
183+
subnets,
184+
securityGroups,
175185
};
176186

177187
// Resolve credential strategy FIRST (new project has no existing credentials)

src/cli/commands/create/command.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { getWorkingDirectory } from '../../../lib';
2-
import type { BuildType, ModelProvider, SDKFramework, TargetLanguage } from '../../../schema';
2+
import type { BuildType, ModelProvider, NetworkMode, SDKFramework, TargetLanguage } from '../../../schema';
33
import { getErrorMessage } from '../../errors';
44
import { COMMAND_DESCRIPTIONS } from '../../tui/copy';
55
import { CreateScreen } from '../../tui/screens/create';
6+
import { parseCommaSeparatedList } from '../shared/vpc-utils';
67
import { type ProgressCallback, createProject, createProjectWithAgent, getDryRunInfo } from './action';
78
import type { CreateOptions } from './types';
89
import { validateCreateOptions } from './validate';
@@ -120,6 +121,9 @@ async function handleCreateCLI(options: CreateOptions): Promise<void> {
120121
modelProvider: options.modelProvider as ModelProvider,
121122
apiKey: options.apiKey,
122123
memory: options.memory as 'none' | 'shortTerm' | 'longAndShortTerm',
124+
networkMode: options.networkMode as NetworkMode | undefined,
125+
subnets: parseCommaSeparatedList(options.subnets),
126+
securityGroups: parseCommaSeparatedList(options.securityGroups),
123127
skipGit: options.skipGit,
124128
skipPythonSetup: options.skipPythonSetup,
125129
onProgress,
@@ -152,6 +156,9 @@ export const registerCreate = (program: Command) => {
152156
.option('--model-provider <provider>', 'Model provider (Bedrock, Anthropic, OpenAI, Gemini) [non-interactive]')
153157
.option('--api-key <key>', 'API key for non-Bedrock providers [non-interactive]')
154158
.option('--memory <option>', 'Memory option (none, shortTerm, longAndShortTerm) [non-interactive]')
159+
.option('--network-mode <mode>', 'Network mode: PUBLIC or VPC (default: PUBLIC) [non-interactive]')
160+
.option('--subnets <ids>', 'Comma-separated subnet IDs (required for VPC mode) [non-interactive]')
161+
.option('--security-groups <ids>', 'Comma-separated security group IDs (required for VPC mode) [non-interactive]')
155162
.option('--output-dir <dir>', 'Output directory (default: current directory) [non-interactive]')
156163
.option('--skip-git', 'Skip git repository initialization [non-interactive]')
157164
.option('--skip-python-setup', 'Skip Python virtual environment setup [non-interactive]')
@@ -179,6 +186,9 @@ export const registerCreate = (program: Command) => {
179186
options.modelProvider ??
180187
options.apiKey ??
181188
options.memory ??
189+
options.networkMode ??
190+
options.subnets ??
191+
options.securityGroups ??
182192
options.outputDir ??
183193
options.skipGit ??
184194
options.skipPythonSetup ??

src/cli/commands/create/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ export interface CreateOptions {
88
modelProvider?: string;
99
apiKey?: string;
1010
memory?: string;
11+
networkMode?: string;
12+
subnets?: string;
13+
securityGroups?: string;
1114
outputDir?: string;
1215
skipGit?: boolean;
1316
skipPythonSetup?: boolean;

src/cli/commands/create/validate.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
TargetLanguageSchema,
77
getSupportedModelProviders,
88
} from '../../../schema';
9+
import { validateVpcOptions } from '../shared/vpc-utils';
910
import type { CreateOptions } from './types';
1011
import { existsSync } from 'fs';
1112
import { join } from 'path';
@@ -121,5 +122,8 @@ export function validateCreateOptions(options: CreateOptions, cwd?: string): Val
121122
}
122123
}
123124

125+
const vpcResult = validateVpcOptions(options);
126+
if (!vpcResult.valid) return vpcResult;
127+
124128
return { valid: true };
125129
}

0 commit comments

Comments
 (0)