diff --git a/src/cli/commands/dev/command.tsx b/src/cli/commands/dev/command.tsx index 427c04a6..f43b62ca 100644 --- a/src/cli/commands/dev/command.tsx +++ b/src/cli/commands/dev/command.tsx @@ -144,6 +144,12 @@ export const registerDev = (program: Command) => { const targetAgent = project.agents.find(a => a.name === config.agentName); const providerInfo = targetAgent?.modelProvider ?? '(see agent code)'; + if (targetAgent?.networkMode === 'VPC') { + console.warn( + 'Warning: This agent uses VPC network mode. Local dev server runs outside your VPC. Network behavior may differ from deployed environment.' + ); + } + console.log(`Starting dev server...`); console.log(`Agent: ${config.agentName}`); console.log(`Provider: ${providerInfo}`); @@ -178,6 +184,20 @@ export const registerDev = (program: Command) => { await new Promise(() => {}); } + // Warn if the target agent uses VPC mode + { + const vpcAgent = opts.agent + ? project.agents.find(a => a.name === opts.agent) + : project.agents.length === 1 + ? project.agents[0] + : undefined; + if (vpcAgent?.networkMode === 'VPC') { + console.warn( + 'Warning: This agent uses VPC network mode. Local dev server runs outside your VPC. Network behavior may differ from deployed environment.' + ); + } + } + // Enter alternate screen buffer for fullscreen mode process.stdout.write(ENTER_ALT_SCREEN); diff --git a/src/cli/commands/invoke/action.ts b/src/cli/commands/invoke/action.ts index 089a869c..b6a14238 100644 --- a/src/cli/commands/invoke/action.ts +++ b/src/cli/commands/invoke/action.ts @@ -67,6 +67,12 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption return { success: false, error: 'No agents defined in configuration' }; } + if (agentSpec.networkMode === 'VPC') { + console.warn( + 'Warning: This agent uses VPC network mode. Invocation may require setting up VPC Endpoints for S3, ECR, Bedrock. If your agent uses a non-Bedrock model provider, VPC will require public internet access.' + ); + } + // Get the deployed state for this specific agent const agentState = targetState?.resources?.agents?.[agentSpec.name]; diff --git a/src/schema/__tests__/constants.test.ts b/src/schema/__tests__/constants.test.ts index 653bef11..893e68c5 100644 --- a/src/schema/__tests__/constants.test.ts +++ b/src/schema/__tests__/constants.test.ts @@ -74,12 +74,12 @@ describe('NetworkModeSchema', () => { expect(NetworkModeSchema.safeParse('PUBLIC').success).toBe(true); }); - it('accepts PRIVATE', () => { - expect(NetworkModeSchema.safeParse('PRIVATE').success).toBe(true); + it('accepts VPC', () => { + expect(NetworkModeSchema.safeParse('VPC').success).toBe(true); }); it('rejects other modes', () => { - expect(NetworkModeSchema.safeParse('VPC').success).toBe(false); + expect(NetworkModeSchema.safeParse('PRIVATE').success).toBe(false); }); }); diff --git a/src/schema/constants.ts b/src/schema/constants.ts index cff05c1b..dc385981 100644 --- a/src/schema/constants.ts +++ b/src/schema/constants.ts @@ -139,5 +139,5 @@ export type NodeRuntime = z.infer; export const RuntimeVersionSchema = z.union([PythonRuntimeSchema, NodeRuntimeSchema]); export type RuntimeVersion = z.infer; -export const NetworkModeSchema = z.enum(['PUBLIC', 'PRIVATE']); +export const NetworkModeSchema = z.enum(['PUBLIC', 'VPC']); export type NetworkMode = z.infer; diff --git a/src/schema/llm-compacted/agentcore.ts b/src/schema/llm-compacted/agentcore.ts index 382fda92..eee63b53 100644 --- a/src/schema/llm-compacted/agentcore.ts +++ b/src/schema/llm-compacted/agentcore.ts @@ -26,10 +26,19 @@ type BuildType = 'CodeZip' | 'Container'; type PythonRuntime = 'PYTHON_3_10' | 'PYTHON_3_11' | 'PYTHON_3_12' | 'PYTHON_3_13'; type NodeRuntime = 'NODE_18' | 'NODE_20' | 'NODE_22'; type RuntimeVersion = PythonRuntime | NodeRuntime; -type NetworkMode = 'PUBLIC' | 'PRIVATE'; +type NetworkMode = 'PUBLIC' | 'VPC'; type MemoryStrategyType = 'SEMANTIC' | 'SUMMARIZATION' | 'USER_PREFERENCE'; type ModelProvider = 'Bedrock' | 'Gemini' | 'OpenAI' | 'Anthropic'; +// ───────────────────────────────────────────────────────────────────────────── +// NETWORK CONFIG +// ───────────────────────────────────────────────────────────────────────────── + +interface NetworkConfig { + subnets: string[]; // @regex ^subnet-[0-9a-zA-Z]{8,17}$ @min 1 @max 16 + securityGroups: string[]; // @regex ^sg-[0-9a-zA-Z]{8,17}$ @min 1 @max 16 +} + // ───────────────────────────────────────────────────────────────────────────── // AGENT // ───────────────────────────────────────────────────────────────────────────── @@ -43,6 +52,7 @@ interface AgentEnvSpec { runtimeVersion: RuntimeVersion; envVars?: EnvVar[]; networkMode?: NetworkMode; // default 'PUBLIC' + networkConfig?: NetworkConfig; // Required when networkMode is 'VPC' instrumentation?: Instrumentation; // OTel settings modelProvider?: ModelProvider; // Model provider used by this agent } diff --git a/src/schema/llm-compacted/mcp.ts b/src/schema/llm-compacted/mcp.ts index 7a65d6ca..4fb4180d 100644 --- a/src/schema/llm-compacted/mcp.ts +++ b/src/schema/llm-compacted/mcp.ts @@ -145,4 +145,4 @@ interface IamPolicyDocument { type GatewayTargetType = 'lambda' | 'mcpServer' | 'openApiSchema' | 'smithyModel'; type PythonRuntime = 'PYTHON_3_10' | 'PYTHON_3_11' | 'PYTHON_3_12' | 'PYTHON_3_13'; type NodeRuntime = 'NODE_18' | 'NODE_20' | 'NODE_22'; -type NetworkMode = 'PUBLIC' | 'PRIVATE'; +type NetworkMode = 'PUBLIC' | 'VPC'; diff --git a/src/schema/schemas/__tests__/agent-env.test.ts b/src/schema/schemas/__tests__/agent-env.test.ts index ebda0437..f317c1eb 100644 --- a/src/schema/schemas/__tests__/agent-env.test.ts +++ b/src/schema/schemas/__tests__/agent-env.test.ts @@ -7,6 +7,7 @@ import { EnvVarSchema, GatewayNameSchema, InstrumentationSchema, + NetworkConfigSchema, } from '../agent-env.js'; import { describe, expect, it } from 'vitest'; @@ -235,13 +236,51 @@ describe('AgentEnvSpecSchema', () => { it('accepts agent with network mode', () => { expect(AgentEnvSpecSchema.safeParse({ ...validPythonAgent, networkMode: 'PUBLIC' }).success).toBe(true); - expect(AgentEnvSpecSchema.safeParse({ ...validPythonAgent, networkMode: 'PRIVATE' }).success).toBe(true); + expect( + AgentEnvSpecSchema.safeParse({ + ...validPythonAgent, + networkMode: 'VPC', + networkConfig: { + subnets: ['subnet-12345678'], + securityGroups: ['sg-12345678'], + }, + }).success + ).toBe(true); }); it('rejects invalid network mode', () => { + expect(AgentEnvSpecSchema.safeParse({ ...validPythonAgent, networkMode: 'PRIVATE' }).success).toBe(false); + }); + + it('rejects VPC mode without networkConfig', () => { expect(AgentEnvSpecSchema.safeParse({ ...validPythonAgent, networkMode: 'VPC' }).success).toBe(false); }); + it('rejects networkConfig without VPC mode', () => { + expect( + AgentEnvSpecSchema.safeParse({ + ...validPythonAgent, + networkMode: 'PUBLIC', + networkConfig: { + subnets: ['subnet-12345678'], + securityGroups: ['sg-12345678'], + }, + }).success + ).toBe(false); + }); + + it('rejects networkConfig with missing networkMode', () => { + expect( + AgentEnvSpecSchema.safeParse({ + ...validPythonAgent, + networkConfig: { + subnets: ['subnet-12345678'], + securityGroups: ['sg-12345678'], + }, + }).success + ).toBe(false); + }); + it('accepts agent with instrumentation config', () => { const result = AgentEnvSpecSchema.safeParse({ ...validPythonAgent, @@ -259,3 +298,53 @@ describe('AgentEnvSpecSchema', () => { expect(AgentEnvSpecSchema.safeParse({ ...validPythonAgent, name: undefined }).success).toBe(false); }); }); + +describe('NetworkConfigSchema', () => { + it('accepts valid network config', () => { + const result = NetworkConfigSchema.safeParse({ + subnets: ['subnet-12345678'], + securityGroups: ['sg-12345678'], + }); + expect(result.success).toBe(true); + }); + + it('accepts multiple subnets and security groups', () => { + const result = NetworkConfigSchema.safeParse({ + subnets: ['subnet-12345678', 'subnet-abcdef12'], + securityGroups: ['sg-12345678', 'sg-abcdef12'], + }); + expect(result.success).toBe(true); + }); + + it('rejects empty subnets array', () => { + const result = NetworkConfigSchema.safeParse({ + subnets: [], + securityGroups: ['sg-12345678'], + }); + expect(result.success).toBe(false); + }); + + it('rejects empty security groups array', () => { + const result = NetworkConfigSchema.safeParse({ + subnets: ['subnet-12345678'], + securityGroups: [], + }); + expect(result.success).toBe(false); + }); + + it('rejects invalid subnet format', () => { + const result = NetworkConfigSchema.safeParse({ + subnets: ['invalid-subnet'], + securityGroups: ['sg-12345678'], + }); + expect(result.success).toBe(false); + }); + + it('rejects invalid security group format', () => { + const result = NetworkConfigSchema.safeParse({ + subnets: ['subnet-12345678'], + securityGroups: ['invalid-sg'], + }); + expect(result.success).toBe(false); + }); +}); diff --git a/src/schema/schemas/__tests__/mcp.test.ts b/src/schema/schemas/__tests__/mcp.test.ts index cd437fd2..296b9eb6 100644 --- a/src/schema/schemas/__tests__/mcp.test.ts +++ b/src/schema/schemas/__tests__/mcp.test.ts @@ -238,8 +238,8 @@ describe('RuntimeConfigSchema', () => { } }); - it('accepts explicit PRIVATE networkMode', () => { - const result = RuntimeConfigSchema.safeParse({ ...validRuntime, networkMode: 'PRIVATE' }); + it('accepts explicit VPC networkMode', () => { + const result = RuntimeConfigSchema.safeParse({ ...validRuntime, networkMode: 'VPC' }); expect(result.success).toBe(true); }); diff --git a/src/schema/schemas/agent-env.ts b/src/schema/schemas/agent-env.ts index cc1d7052..05e70278 100644 --- a/src/schema/schemas/agent-env.ts +++ b/src/schema/schemas/agent-env.ts @@ -103,25 +103,60 @@ export const InstrumentationSchema = z.object({ }); export type Instrumentation = z.infer; +/** + * VPC network configuration for agents running in VPC mode. + * Requires at least one subnet and one security group. + */ +export const NetworkConfigSchema = z.object({ + subnets: z + .array(z.string().regex(/^subnet-[0-9a-zA-Z]{8,17}$/)) + .min(1) + .max(16), + securityGroups: z + .array(z.string().regex(/^sg-[0-9a-zA-Z]{8,17}$/)) + .min(1) + .max(16), +}); +export type NetworkConfig = z.infer; + /** * AgentEnvSpec - represents an AgentCore Runtime. * This is a top-level resource in the schema. */ -export const AgentEnvSpecSchema = z.object({ - type: AgentTypeSchema, - name: AgentNameSchema, - build: BuildTypeSchema, - entrypoint: EntrypointSchema, - codeLocation: DirectoryPathSchema, - runtimeVersion: RuntimeVersionSchemaFromConstants, - /** Environment variables to set on the runtime */ - envVars: z.array(EnvVarSchema).optional(), - /** Network mode for the runtime. Defaults to PUBLIC. */ - networkMode: NetworkModeSchema.optional(), - /** Instrumentation settings for observability. Defaults to OTel enabled. */ - instrumentation: InstrumentationSchema.optional(), - /** Model provider used by this agent. Optional for backwards compatibility. */ - modelProvider: ModelProviderSchema.optional(), -}); +export const AgentEnvSpecSchema = z + .object({ + type: AgentTypeSchema, + name: AgentNameSchema, + build: BuildTypeSchema, + entrypoint: EntrypointSchema, + codeLocation: DirectoryPathSchema, + runtimeVersion: RuntimeVersionSchemaFromConstants, + /** Environment variables to set on the runtime */ + envVars: z.array(EnvVarSchema).optional(), + /** Network mode for the runtime. Defaults to PUBLIC. */ + networkMode: NetworkModeSchema.optional(), + /** VPC network configuration. Required when networkMode is VPC. */ + networkConfig: NetworkConfigSchema.optional(), + /** Instrumentation settings for observability. Defaults to OTel enabled. */ + instrumentation: InstrumentationSchema.optional(), + /** Model provider used by this agent. Optional for backwards compatibility. */ + modelProvider: ModelProviderSchema.optional(), + }) + .superRefine((data, ctx) => { + if (data.networkMode === 'VPC' && !data.networkConfig) { + ctx.addIssue({ + code: 'custom', + path: ['networkConfig'], + message: 'networkConfig is required when networkMode is VPC', + }); + } + if (data.networkMode !== 'VPC' && data.networkConfig) { + ctx.addIssue({ + code: 'custom', + path: ['networkConfig'], + message: 'networkConfig is only allowed when networkMode is VPC', + }); + } + }); export type AgentEnvSpec = z.infer;