diff --git a/src/cli/commands/add/__tests__/validate.test.ts b/src/cli/commands/add/__tests__/validate.test.ts index cefc1a73..d02ee2d0 100644 --- a/src/cli/commands/add/__tests__/validate.test.ts +++ b/src/cli/commands/add/__tests__/validate.test.ts @@ -162,6 +162,52 @@ describe('validate', () => { expect(validateAddAgentOptions(validAgentOptionsByo)).toEqual({ valid: true }); expect(validateAddAgentOptions(validAgentOptionsCreate)).toEqual({ valid: true }); }); + + // VPC validation tests + it('rejects invalid network mode', () => { + const result = validateAddAgentOptions({ ...validAgentOptionsCreate, networkMode: 'INVALID' as any }); + expect(result.valid).toBe(false); + expect(result.error).toContain('Invalid network mode'); + }); + + it('rejects VPC mode without subnets', () => { + const result = validateAddAgentOptions({ + ...validAgentOptionsCreate, + networkMode: 'VPC', + securityGroups: 'sg-12345678', + }); + expect(result.valid).toBe(false); + expect(result.error).toContain('--subnets is required'); + }); + + it('rejects VPC mode without security groups', () => { + const result = validateAddAgentOptions({ + ...validAgentOptionsCreate, + networkMode: 'VPC', + subnets: 'subnet-12345678', + }); + expect(result.valid).toBe(false); + expect(result.error).toContain('--security-groups is required'); + }); + + it('rejects subnets without VPC mode', () => { + const result = validateAddAgentOptions({ + ...validAgentOptionsCreate, + subnets: 'subnet-12345678', + }); + expect(result.valid).toBe(false); + expect(result.error).toContain('require --network-mode VPC'); + }); + + it('passes for valid VPC options', () => { + const result = validateAddAgentOptions({ + ...validAgentOptionsCreate, + networkMode: 'VPC', + subnets: 'subnet-12345678', + securityGroups: 'sg-12345678', + }); + expect(result.valid).toBe(true); + }); }); describe('validateAddGatewayOptions', () => { diff --git a/src/cli/commands/add/actions.ts b/src/cli/commands/add/actions.ts index 1c94737e..a30933cb 100644 --- a/src/cli/commands/add/actions.ts +++ b/src/cli/commands/add/actions.ts @@ -7,6 +7,7 @@ import type { GatewayAuthorizerType, MemoryStrategyType, ModelProvider, + NetworkMode, SDKFramework, TargetLanguage, } from '../../../schema'; @@ -29,6 +30,7 @@ import { createRenderer } from '../../templates'; import type { MemoryOption } from '../../tui/screens/generate/types'; import type { AddGatewayConfig, AddMcpToolConfig } from '../../tui/screens/mcp/types'; import { DEFAULT_EVENT_EXPIRY } from '../../tui/screens/memory/types'; +import { parseCommaSeparatedList } from '../shared/vpc-utils'; import type { AddAgentResult, AddGatewayResult, AddIdentityResult, AddMcpToolResult, AddMemoryResult } from './types'; import { mkdirSync } from 'fs'; import { dirname, join } from 'path'; @@ -43,6 +45,9 @@ export interface ValidatedAddAgentOptions { modelProvider: ModelProvider; apiKey?: string; memory?: MemoryOption; + networkMode?: NetworkMode; + subnets?: string; + securityGroups?: string; codeLocation?: string; entrypoint?: string; } @@ -120,6 +125,9 @@ async function handleCreatePath(options: ValidatedAddAgentOptions, configBaseDir modelProvider: options.modelProvider, memory: options.memory!, language: options.language, + networkMode: options.networkMode, + subnets: parseCommaSeparatedList(options.subnets), + securityGroups: parseCommaSeparatedList(options.securityGroups), }; const agentPath = join(projectRoot, APP_DIR, options.name); @@ -186,6 +194,7 @@ async function handleByoPath( const project = await configIO.readProjectSpec(); + const networkMode = options.networkMode ?? 'PUBLIC'; const agent: AgentEnvSpec = { type: 'AgentCoreRuntime', name: options.name, @@ -193,7 +202,15 @@ async function handleByoPath( entrypoint: (options.entrypoint ?? 'main.py') as FilePath, codeLocation: codeLocation as DirectoryPath, runtimeVersion: 'PYTHON_3_12', - networkMode: 'PUBLIC', + networkMode, + ...(networkMode === 'VPC' && options.subnets && options.securityGroups + ? { + networkConfig: { + subnets: parseCommaSeparatedList(options.subnets)!, + securityGroups: parseCommaSeparatedList(options.securityGroups)!, + }, + } + : {}), }; project.agents.push(agent); diff --git a/src/cli/commands/add/command.tsx b/src/cli/commands/add/command.tsx index 58a95503..a471041a 100644 --- a/src/cli/commands/add/command.tsx +++ b/src/cli/commands/add/command.tsx @@ -40,6 +40,9 @@ async function handleAddAgentCLI(options: AddAgentOptions): Promise { modelProvider: options.modelProvider!, apiKey: options.apiKey, memory: options.memory, + networkMode: options.networkMode, + subnets: options.subnets, + securityGroups: options.securityGroups, codeLocation: options.codeLocation, entrypoint: options.entrypoint, }); @@ -227,6 +230,9 @@ export function registerAdd(program: Command) { .option('--model-provider ', 'Model provider: Bedrock, Anthropic, OpenAI, Gemini [non-interactive]') .option('--api-key ', 'API key for non-Bedrock providers [non-interactive]') .option('--memory ', 'Memory: none, shortTerm, longAndShortTerm (create path only) [non-interactive]') + .option('--network-mode ', 'Network mode: PUBLIC or VPC (default: PUBLIC) [non-interactive]') + .option('--subnets ', 'Comma-separated subnet IDs (required for VPC mode) [non-interactive]') + .option('--security-groups ', 'Comma-separated security group IDs (required for VPC mode) [non-interactive]') .option('--code-location ', 'Path to existing code (BYO path only) [non-interactive]') .option('--entrypoint ', 'Entry file relative to code-location (BYO, default: main.py) [non-interactive]') .option('--json', 'Output as JSON [non-interactive]') diff --git a/src/cli/commands/add/types.ts b/src/cli/commands/add/types.ts index f20c3b01..9bcc4b60 100644 --- a/src/cli/commands/add/types.ts +++ b/src/cli/commands/add/types.ts @@ -1,4 +1,4 @@ -import type { GatewayAuthorizerType, ModelProvider, SDKFramework, TargetLanguage } from '../../../schema'; +import type { GatewayAuthorizerType, ModelProvider, NetworkMode, SDKFramework, TargetLanguage } from '../../../schema'; import type { MemoryOption } from '../../tui/screens/generate/types'; // Agent types @@ -11,6 +11,9 @@ export interface AddAgentOptions { modelProvider?: ModelProvider; apiKey?: string; memory?: MemoryOption; + networkMode?: NetworkMode; + subnets?: string; + securityGroups?: string; codeLocation?: string; entrypoint?: string; json?: boolean; diff --git a/src/cli/commands/add/validate.ts b/src/cli/commands/add/validate.ts index 7ad3de1c..5e4357b5 100644 --- a/src/cli/commands/add/validate.ts +++ b/src/cli/commands/add/validate.ts @@ -7,6 +7,7 @@ import { TargetLanguageSchema, getSupportedModelProviders, } from '../../../schema'; +import { validateVpcOptions } from '../shared/vpc-utils'; import type { AddAgentOptions, AddGatewayOptions, @@ -102,6 +103,9 @@ export function validateAddAgentOptions(options: AddAgentOptions): ValidationRes } } + const vpcResult = validateVpcOptions(options); + if (!vpcResult.valid) return vpcResult; + return { valid: true }; } diff --git a/src/cli/commands/create/__tests__/validate.test.ts b/src/cli/commands/create/__tests__/validate.test.ts index a33b0609..520afd33 100644 --- a/src/cli/commands/create/__tests__/validate.test.ts +++ b/src/cli/commands/create/__tests__/validate.test.ts @@ -132,6 +132,90 @@ describe('validateCreateOptions', () => { expect(result.valid).toBe(true); }); + // VPC validation tests + it('rejects invalid network mode', () => { + const result = validateCreateOptions( + { + name: 'VpcTest1', + language: 'Python', + framework: 'Strands', + modelProvider: 'Bedrock', + memory: 'none', + networkMode: 'INVALID', + }, + testDir + ); + expect(result.valid).toBe(false); + expect(result.error).toContain('Invalid network mode'); + }); + + it('rejects VPC mode without subnets', () => { + const result = validateCreateOptions( + { + name: 'VpcTest2', + language: 'Python', + framework: 'Strands', + modelProvider: 'Bedrock', + memory: 'none', + networkMode: 'VPC', + securityGroups: 'sg-12345678', + }, + testDir + ); + expect(result.valid).toBe(false); + expect(result.error).toContain('--subnets is required'); + }); + + it('rejects VPC mode without security groups', () => { + const result = validateCreateOptions( + { + name: 'VpcTest3', + language: 'Python', + framework: 'Strands', + modelProvider: 'Bedrock', + memory: 'none', + networkMode: 'VPC', + subnets: 'subnet-12345678', + }, + testDir + ); + expect(result.valid).toBe(false); + expect(result.error).toContain('--security-groups is required'); + }); + + it('rejects subnets without VPC mode', () => { + const result = validateCreateOptions( + { + name: 'VpcTest4', + language: 'Python', + framework: 'Strands', + modelProvider: 'Bedrock', + memory: 'none', + subnets: 'subnet-12345678', + }, + testDir + ); + expect(result.valid).toBe(false); + expect(result.error).toContain('require --network-mode VPC'); + }); + + it('returns valid with VPC mode and required options', () => { + const result = validateCreateOptions( + { + name: 'VpcTest5', + language: 'Python', + framework: 'Strands', + modelProvider: 'Bedrock', + memory: 'none', + networkMode: 'VPC', + subnets: 'subnet-12345678', + securityGroups: 'sg-12345678', + }, + testDir + ); + expect(result.valid).toBe(true); + }); + it('returns invalid for unsupported framework/model combination', () => { // GoogleADK only supports certain providers, not all const result = validateCreateOptions( diff --git a/src/cli/commands/create/action.ts b/src/cli/commands/create/action.ts index da4d40c2..4da9641b 100644 --- a/src/cli/commands/create/action.ts +++ b/src/cli/commands/create/action.ts @@ -4,6 +4,7 @@ import type { BuildType, DeployedState, ModelProvider, + NetworkMode, SDKFramework, TargetLanguage, } from '../../../schema'; @@ -120,6 +121,9 @@ export interface CreateWithAgentOptions { modelProvider: ModelProvider; apiKey?: string; memory: MemoryOption; + networkMode?: NetworkMode; + subnets?: string[]; + securityGroups?: string[]; skipGit?: boolean; skipPythonSetup?: boolean; onProgress?: ProgressCallback; @@ -135,6 +139,9 @@ export async function createProjectWithAgent(options: CreateWithAgentOptions): P modelProvider, apiKey, memory, + networkMode, + subnets, + securityGroups, skipGit, skipPythonSetup, onProgress, @@ -172,6 +179,9 @@ export async function createProjectWithAgent(options: CreateWithAgentOptions): P apiKey, memory, language, + networkMode, + subnets, + securityGroups, }; // Resolve credential strategy FIRST (new project has no existing credentials) diff --git a/src/cli/commands/create/command.tsx b/src/cli/commands/create/command.tsx index ada69dd8..60558461 100644 --- a/src/cli/commands/create/command.tsx +++ b/src/cli/commands/create/command.tsx @@ -1,8 +1,9 @@ import { getWorkingDirectory } from '../../../lib'; -import type { BuildType, ModelProvider, SDKFramework, TargetLanguage } from '../../../schema'; +import type { BuildType, ModelProvider, NetworkMode, SDKFramework, TargetLanguage } from '../../../schema'; import { getErrorMessage } from '../../errors'; import { COMMAND_DESCRIPTIONS } from '../../tui/copy'; import { CreateScreen } from '../../tui/screens/create'; +import { parseCommaSeparatedList } from '../shared/vpc-utils'; import { type ProgressCallback, createProject, createProjectWithAgent, getDryRunInfo } from './action'; import type { CreateOptions } from './types'; import { validateCreateOptions } from './validate'; @@ -120,6 +121,9 @@ async function handleCreateCLI(options: CreateOptions): Promise { modelProvider: options.modelProvider as ModelProvider, apiKey: options.apiKey, memory: options.memory as 'none' | 'shortTerm' | 'longAndShortTerm', + networkMode: options.networkMode as NetworkMode | undefined, + subnets: parseCommaSeparatedList(options.subnets), + securityGroups: parseCommaSeparatedList(options.securityGroups), skipGit: options.skipGit, skipPythonSetup: options.skipPythonSetup, onProgress, @@ -152,6 +156,9 @@ export const registerCreate = (program: Command) => { .option('--model-provider ', 'Model provider (Bedrock, Anthropic, OpenAI, Gemini) [non-interactive]') .option('--api-key ', 'API key for non-Bedrock providers [non-interactive]') .option('--memory