Skip to content

Commit c75f4cd

Browse files
authored
feat: add VPC CLI flags to create and add commands [2/3] (#425)
* feat: add VPC network mode to schema Fix NetworkModeSchema enum from PUBLIC|PRIVATE to PUBLIC|VPC to match the AWS API. Add NetworkConfigSchema for subnet and security group validation, and networkConfig field to AgentEnvSpec with cross-field validation requiring networkConfig when networkMode is VPC. Changes: - Fix NetworkModeSchema enum: PRIVATE → VPC - Add NetworkConfigSchema (subnet/SG ID regex, min/max array bounds) - Add networkConfig optional field to AgentEnvSpec - Add superRefine cross-field validation - Update LLM-compacted schema documentation - Add 13 new unit tests for VPC schema validation * feat: add VPC CLI flags to create and add commands Add --network-mode, --subnets, and --security-groups flags to both the create and add agent CLI commands. Extract shared VPC parsing and validation utilities. Propagate VPC configuration through GenerateConfig and schema mapper to AgentEnvSpec. Changes: - Add shared/vpc-utils.ts with parseCommaSeparatedList and validateVpcOptions helpers - Add VPC fields to GenerateConfig and CreateOptions/AddAgentOptions - Update schema-mapper to propagate VPC fields to AgentEnvSpec - Add VPC flags to create command with validation - Add VPC flags to add agent command with validation - Update BYO agent path to support VPC configuration - Add 13 new unit tests for CLI validation and schema mapping
1 parent 7a81b02 commit c75f4cd

14 files changed

Lines changed: 288 additions & 6 deletions

File tree

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)