From b13c8a02334b4f70325cff4165d3b44896e76424 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Mon, 17 Aug 2026 21:10:03 +0000 Subject: [PATCH 1/9] feat(proj): add handler scaffolding for runtime --- src/handlers/project/add/index.ts | 2 + src/handlers/project/add/runtime/index.ts | 343 ++++++++++++++++++++++ src/handlers/project/types.ts | 2 +- src/projectSchemas/runtime.ts | 6 +- 4 files changed, 349 insertions(+), 4 deletions(-) create mode 100644 src/handlers/project/add/runtime/index.ts diff --git a/src/handlers/project/add/index.ts b/src/handlers/project/add/index.ts index 8545ccba3..2d7970bfa 100644 --- a/src/handlers/project/add/index.ts +++ b/src/handlers/project/add/index.ts @@ -1,11 +1,13 @@ import { withProject } from "../../../middleware/"; import { Router } from "../../../router"; import { createAddHarnessHandler } from "./harness"; +import { createAddRuntimeHandler } from "./runtime"; import type { AddProjectResourceConfig } from "./types"; export function createAddProjectResourceHandler(config: AddProjectResourceConfig): Router { const projectAdd = new Router("add", "add project resources"); projectAdd.use(withProject({ projectManager: config.projectManager, cwd: process.cwd() })); projectAdd.handler(createAddHarnessHandler(config)); + projectAdd.handler(createAddRuntimeHandler(config)); return projectAdd; } diff --git a/src/handlers/project/add/runtime/index.ts b/src/handlers/project/add/runtime/index.ts new file mode 100644 index 000000000..a87f7d15c --- /dev/null +++ b/src/handlers/project/add/runtime/index.ts @@ -0,0 +1,343 @@ +import z from "zod"; +import { createHandler, flag, ProjectKey } from "../../../../router"; +import type { AddProjectResourceConfig } from "../types"; +import { parseJsonFlag } from "../../../utils"; +import { InputValidationError } from "../../../../errors"; +import type { + AgentRuntimeArtifact, + AuthorizerConfiguration, + FilesystemConfiguration, + LifecycleConfiguration, + NetworkConfiguration, + ProtocolConfiguration, + RequestHeaderConfiguration, +} from "@aws-sdk/client-bedrock-agentcore-control"; +import { + type BuildType, + type EnvVar, + type FilesystemConfiguration as ProjectFilesystemConfiguration, + type LifecycleConfiguration as ProjectLifecycleConfiguration, + type NetworkConfig, + BuildTypeSchema, +} from "../../../../projectSchemas/runtime"; +import type { AuthorizerConfig, RuntimeAuthorizerType } from "../../../../projectSchemas/auth"; +import type { + NetworkMode, + ProtocolMode, + RuntimeVersion, +} from "../../../../projectSchemas/constants"; + +export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => + createHandler({ + name: "runtime", + description: "adds a runtime to the current project", + flags: [ + flag("name", "the name of the runtime", z.string().optional()), + flag("description", "description of the runtime", z.string().optional()), + flag( + "role-arn", + "IAM role ARN that provides permissions for the runtime", + z.string().optional(), + ), + flag( + "agent-runtime-artifact", + "runtime artifact configuration (JSON AgentRuntimeArtifact)", + z.string().optional(), + ), + flag( + "network-configuration", + "network configuration (JSON NetworkConfiguration)", + z.string().optional(), + ), + flag( + "authorizer-configuration", + "inbound authorizer configuration (JSON AuthorizerConfiguration)", + z.string().optional(), + ), + flag( + "protocol-configuration", + "protocol configuration (JSON ProtocolConfiguration)", + z.string().optional(), + ), + flag( + "request-header-configuration", + "request header passthrough configuration (JSON RequestHeaderConfiguration)", + z.string().optional(), + ), + flag( + "lifecycle-configuration", + "lifecycle configuration (JSON LifecycleConfiguration)", + z.string().optional(), + ), + flag( + "environment-variables", + "environment variables (JSON object of key/value strings)", + z.string().optional(), + ), + flag( + "filesystem-configurations", + "filesystem mount configurations (JSON FilesystemConfiguration[])", + z.string().optional(), + ), + flag("tags", "tags to apply (JSON object of key/value strings)", z.string().optional()), + flag("build", "build type for the runtime source code", BuildTypeSchema.optional()), + flag("entrypoint", "entrypoint for a codezip runtime", z.string().optional()), + flag( + "dockerfile", + "dockerfile describing the container for the runtime", + z.string().optional(), + ), + flag( + "build-context-path", + "docker build context directory relative to project root (Container only)", + z.string().optional(), + ), + flag( + "custom-docker-build-args", + "docker build args (JSON object of key/value strings, Container only)", + z.string().optional(), + ), + flag( + "instrumentation", + 'instrumentation config (JSON, e.g. {"enableOtel": true})', + z.string().optional(), + ), + flag( + "additional-policies", + "additional IAM policy ARNs or policy document paths", + z.array(z.string()).optional(), + ), + ], + handle: async (ctx, flags) => { + if (!flags.name) + throw new InputValidationError("required option '--name ' not specified"); + + const inputArtifact = parseJsonFlag( + "agent-runtime-artifact", + flags["agent-runtime-artifact"], + ); + const inputNetwork = parseJsonFlag( + "network-configuration", + flags["network-configuration"], + ); + const inputAuthConfig = parseJsonFlag( + "authorizer-configuration", + flags["authorizer-configuration"], + ); + const inputProtocol = parseJsonFlag( + "protocol-configuration", + flags["protocol-configuration"], + ); + const inputRequestHeaders = parseJsonFlag( + "request-header-configuration", + flags["request-header-configuration"], + ); + const inputLifecycle = parseJsonFlag( + "lifecycle-configuration", + flags["lifecycle-configuration"], + ); + const inputFilesystems = parseJsonFlag( + "filesystem-configurations", + flags["filesystem-configurations"], + ); + const inputEnvironmentVariables = parseJsonFlag>( + "environment-variables", + flags["environment-variables"], + ); + + const build = getBuildType(flags.build, inputArtifact); + const entrypoint = getEntrypoint(flags.entrypoint, inputArtifact); + + if (entrypoint && build === "Container") + throw new InputValidationError( + `code entrypoint cannot be provided with Container build type`, + ); + + if (flags["custom-docker-build-args"] && !flags.dockerfile && !flags["build-context-path"]) + throw new InputValidationError( + " --custom-docker-build-args requires --dockerfile or --build-context-path", + ); + + const network = toNetwork(inputNetwork); + const auth = toAuthorizer(inputAuthConfig); + const protocol = toProtocol(inputProtocol); + const requestHeaderAllowlist = toRequestHeaderAllowlist(inputRequestHeaders); + const lifecycleConfiguration = toLifecycle(inputLifecycle); + const filesystemConfigurations = toFilesystems(inputFilesystems); + + const runtimeConfig = { + name: flags.name, + description: flags.description, + build, + entrypoint, + runtimeVersion: getRuntimeVersion(inputArtifact), + dockerfile: flags.dockerfile, + buildContextPath: flags["build-context-path"], + customDockerBuildArgs: parseJsonFlag>( + "custom-docker-build-args", + flags["custom-docker-build-args"], + ), + executionRoleArn: flags["role-arn"], + additionalPolicies: flags["additional-policies"], + instrumentation: parseJsonFlag<{ enableOtel: boolean }>( + "instrumentation", + flags["instrumentation"], + ), + envVars: toEnvironmentVariables(inputEnvironmentVariables), + networkMode: network?.networkMode, + networkConfig: network?.networkConfig, + authorizerType: auth?.authorizerType, + authorizerConfiguration: auth?.authorizerConfiguration, + protocol, + requestHeaderAllowlist, + lifecycleConfiguration, + filesystemConfigurations, + tags: parseJsonFlag>("tags", flags["tags"]), + }; + + const project = ctx.require(ProjectKey); + for await (const event of config.projectManager.addResource(project, { + resourceType: "runtime", + resourceConfig: runtimeConfig, + })) { + config.io.stderr.write(`${event.message}\n`); + } + + config.io.stderr.write(`added runtime '${flags.name}' to '${project.name}'\n`); + }, + }); + +/** Converts AgentRuntimeArtifact union discriminator to project schema BuildType. */ +function getBuildType( + buildFlag: z.input | undefined, + runtimeArtifact: AgentRuntimeArtifact | undefined, +): BuildType { + if (runtimeArtifact && buildFlag) + throw new InputValidationError(`--runtime-artifact and --build are mutually exclusive`); + if (buildFlag) return buildFlag; + if (runtimeArtifact?.containerConfiguration) return "Container"; + if (runtimeArtifact?.codeConfiguration) return "CodeZip"; + throw new InputValidationError(`exactly one of --runtime-artifact and --build is required`); +} + +/** Converts codeConfiguration.entryPoint string[] to colon-joined "file.py:handler" format. */ +function getEntrypoint( + entrypointFlag: string | undefined, + runtimeArtifact: AgentRuntimeArtifact | undefined, +): string | undefined { + if (runtimeArtifact && entrypointFlag) + throw new InputValidationError(`--runtime-artifact and --entrypoint are mutually exclusive`); + if (entrypointFlag) return entrypointFlag; + if (runtimeArtifact?.codeConfiguration?.entryPoint) + return runtimeArtifact.codeConfiguration.entryPoint.join(":"); + return undefined; +} + +/** Converts codeConfiguration.runtime enum string to project schema RuntimeVersion. */ +function getRuntimeVersion(artifact: AgentRuntimeArtifact | undefined): RuntimeVersion | undefined { + if (!artifact?.codeConfiguration?.runtime) return undefined; + return artifact.codeConfiguration.runtime as RuntimeVersion; +} + +/** Converts API flat {key: value} map to project schema [{name, value}] array. */ +function toEnvironmentVariables(envVars: Record | undefined): EnvVar[] { + return envVars ? Object.entries(envVars).map(([name, value]) => ({ name, value })) : []; +} + +/** Converts API NetworkConfiguration to project schema networkMode + networkConfig fields. */ +function toNetwork( + network: NetworkConfiguration | undefined, +): { networkMode: NetworkMode; networkConfig: NetworkConfig | undefined } | undefined { + if (!network) return undefined; + return { + networkMode: network.networkMode as NetworkMode, + networkConfig: network.networkModeConfig + ? { + subnets: network.networkModeConfig.subnets ?? [], + securityGroups: network.networkModeConfig.securityGroups ?? [], + } + : undefined, + }; +} + +/** Converts API AuthorizerConfiguration union to project schema authorizerType + authorizerConfiguration. */ +function toAuthorizer( + auth: AuthorizerConfiguration | undefined, +): + { authorizerType: RuntimeAuthorizerType; authorizerConfiguration: AuthorizerConfig } | undefined { + if (!auth) return undefined; + if ("customJWTAuthorizer" in auth && auth.customJWTAuthorizer) { + const c = auth.customJWTAuthorizer; + if (!c.discoveryUrl) + throw new InputValidationError("discoveryUrl is required in authorizer configuration"); + return { + authorizerType: "CUSTOM_JWT", + authorizerConfiguration: { + customJwtAuthorizer: { + discoveryUrl: c.discoveryUrl, + allowedAudience: c.allowedAudience, + allowedClients: c.allowedClients, + allowedScopes: c.allowedScopes, + }, + }, + }; + } + throw new InputValidationError("Unrecognized authorizer configuration variant"); +} + +/** Converts API ProtocolConfiguration wrapper to project schema ProtocolMode enum. */ +function toProtocol(protocol: ProtocolConfiguration | undefined): ProtocolMode | undefined { + if (!protocol) return undefined; + return protocol.serverProtocol as ProtocolMode; +} + +/** Unwraps API RequestHeaderConfiguration union to project schema string[]. */ +function toRequestHeaderAllowlist( + headers: RequestHeaderConfiguration | undefined, +): string[] | undefined { + if (!headers) return undefined; + if ("requestHeaderAllowlist" in headers && headers.requestHeaderAllowlist) { + return headers.requestHeaderAllowlist; + } + return undefined; +} + +/** Converts API LifecycleConfiguration to project schema LifecycleConfiguration. */ +function toLifecycle( + lifecycle: LifecycleConfiguration | undefined, +): ProjectLifecycleConfiguration | undefined { + if (!lifecycle) return undefined; + return { + idleRuntimeSessionTimeout: lifecycle.idleRuntimeSessionTimeout, + maxLifetime: lifecycle.maxLifetime, + }; +} + +/** Converts API FilesystemConfiguration[] tagged unions to project schema format. */ +function toFilesystems( + filesystems: FilesystemConfiguration[] | undefined, +): ProjectFilesystemConfiguration[] | undefined { + if (!filesystems || filesystems.length === 0) return undefined; + return filesystems.map((fs): ProjectFilesystemConfiguration => { + if ("sessionStorage" in fs && fs.sessionStorage) { + return { sessionStorage: { mountPath: fs.sessionStorage.mountPath! } }; + } + if ("efsAccessPoint" in fs && fs.efsAccessPoint) { + return { + efsAccessPoint: { + accessPointArn: fs.efsAccessPoint.accessPointArn!, + mountPath: fs.efsAccessPoint.mountPath!, + }, + }; + } + if ("s3FilesAccessPoint" in fs && fs.s3FilesAccessPoint) { + return { + s3FilesAccessPoint: { + accessPointArn: fs.s3FilesAccessPoint.accessPointArn!, + mountPath: fs.s3FilesAccessPoint.mountPath!, + }, + }; + } + throw new InputValidationError("Unrecognized filesystem configuration variant"); + }); +} diff --git a/src/handlers/project/types.ts b/src/handlers/project/types.ts index c187abb96..0f6692fea 100644 --- a/src/handlers/project/types.ts +++ b/src/handlers/project/types.ts @@ -48,7 +48,7 @@ export type AddResourceInput = } | { resourceType: "runtime"; - resourceConfig: z.input; + resourceConfig: Omit, "codeLocation" | "runtimeVersion">; }; export type ProjectResource = AddResourceInput["resourceType"]; diff --git a/src/projectSchemas/runtime.ts b/src/projectSchemas/runtime.ts index 61dc95886..33cb295da 100644 --- a/src/projectSchemas/runtime.ts +++ b/src/projectSchemas/runtime.ts @@ -8,7 +8,7 @@ import { VPC_ID_PATTERN, isContainerBuild, } from "./constants"; -import type { DirectoryPath, FilePath } from "./types"; +import type { DirectoryPath } from "./types"; import { AuthorizerConfigSchema, RuntimeAuthorizerTypeSchema } from "./auth"; import { ConnectionSchema } from "./connections"; import { TagsSchema } from "./tags"; @@ -49,7 +49,7 @@ export const EntrypointSchema = z .regex( /^[a-zA-Z0-9_][a-zA-Z0-9_/.-]*\.(py|ts|js)(:[a-zA-Z_][a-zA-Z0-9_]*)?$/, 'Must be a Python (.py) or TypeScript (.ts/.js) file path with optional handler (e.g., "main.py:handler" or "index.ts")', - ) as unknown as z.ZodType; + ); const DirectoryPathSchema = z.string().min(1) as unknown as z.ZodType; const DOCKERFILE_PATH_ALLOWED_CHARS = /^[A-Za-z0-9._/-]+$/; export function isValidDockerfilePath(p: string): boolean { @@ -241,7 +241,7 @@ export const ProjectRuntimeSchema = z name: AgentNameSchema, description: z.string().max(200).optional(), build: BuildTypeSchema, - entrypoint: EntrypointSchema, + entrypoint: EntrypointSchema.optional(), codeLocation: DirectoryPathSchema, dockerfile: DockerfilePathSchema.optional(), buildContextPath: DirectoryPathSchema.optional(), From f9714a49f0dd0eb923ac42b8aa0de60723f3c638 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Tue, 18 Aug 2026 00:39:45 +0000 Subject: [PATCH 2/9] refactor(runtime): simplify add runtime to leverage a template setup --- src/handlers/project/add/runtime/index.ts | 169 ++++++------- src/handlers/project/project.test.ts | 284 +++++++++++++++++++++- src/handlers/project/types.ts | 61 ++++- 3 files changed, 414 insertions(+), 100 deletions(-) diff --git a/src/handlers/project/add/runtime/index.ts b/src/handlers/project/add/runtime/index.ts index a87f7d15c..ead37907c 100644 --- a/src/handlers/project/add/runtime/index.ts +++ b/src/handlers/project/add/runtime/index.ts @@ -4,7 +4,6 @@ import type { AddProjectResourceConfig } from "../types"; import { parseJsonFlag } from "../../../utils"; import { InputValidationError } from "../../../../errors"; import type { - AgentRuntimeArtifact, AuthorizerConfiguration, FilesystemConfiguration, LifecycleConfiguration, @@ -13,7 +12,6 @@ import type { RequestHeaderConfiguration, } from "@aws-sdk/client-bedrock-agentcore-control"; import { - type BuildType, type EnvVar, type FilesystemConfiguration as ProjectFilesystemConfiguration, type LifecycleConfiguration as ProjectLifecycleConfiguration, @@ -21,34 +19,64 @@ import { BuildTypeSchema, } from "../../../../projectSchemas/runtime"; import type { AuthorizerConfig, RuntimeAuthorizerType } from "../../../../projectSchemas/auth"; -import type { - NetworkMode, - ProtocolMode, - RuntimeVersion, +import { + type NetworkMode, + type ProtocolMode, + RuntimeVersionSchema, } from "../../../../projectSchemas/constants"; +import { RUNTIME_TEMPLATES } from "../../types"; export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => createHandler({ name: "runtime", - description: "adds a runtime to the current project", + description: "adds a runtime to the current project either from a template or BYO", flags: [ flag("name", "the name of the runtime", z.string().optional()), flag("description", "description of the runtime", z.string().optional()), + flag("template", "runtime template to scaffold from", z.enum(RUNTIME_TEMPLATES).optional()), + flag("code-location", "path to existing agent source code (BYO path)", z.string().optional()), + flag("build", "build type: CodeZip or Container (BYO only)", BuildTypeSchema.optional()), + flag("entrypoint", "entrypoint file, e.g. main.py:handler (BYO only)", z.string().optional()), + flag( + "runtime-version", + "language runtime, e.g. PYTHON_3_13, NODE_22 (BYO CodeZip only)", + RuntimeVersionSchema.optional(), + ), + flag( + "dockerfile", + "dockerfile for the container build (BYO Container only)", + z.string().optional(), + ), + flag( + "build-context-path", + "docker build context directory relative to project root (BYO Container only)", + z.string().optional(), + ), + flag( + "custom-docker-build-args", + "docker build args as JSON key/value object (BYO Container only)", + z.string().optional(), + ), flag( "role-arn", "IAM role ARN that provides permissions for the runtime", z.string().optional(), ), flag( - "agent-runtime-artifact", - "runtime artifact configuration (JSON AgentRuntimeArtifact)", - z.string().optional(), + "additional-policies", + "additional IAM policy ARNs or policy document paths", + z.array(z.string()).optional(), ), flag( "network-configuration", "network configuration (JSON NetworkConfiguration)", z.string().optional(), ), + flag( + "vpc-id", + "VPC ID for Container builds in VPC mode (CodeBuild cannot infer it from subnets)", + z.string().optional(), + ), flag( "authorizer-configuration", "inbound authorizer configuration (JSON AuthorizerConfiguration)", @@ -80,42 +108,15 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => z.string().optional(), ), flag("tags", "tags to apply (JSON object of key/value strings)", z.string().optional()), - flag("build", "build type for the runtime source code", BuildTypeSchema.optional()), - flag("entrypoint", "entrypoint for a codezip runtime", z.string().optional()), - flag( - "dockerfile", - "dockerfile describing the container for the runtime", - z.string().optional(), - ), - flag( - "build-context-path", - "docker build context directory relative to project root (Container only)", - z.string().optional(), - ), - flag( - "custom-docker-build-args", - "docker build args (JSON object of key/value strings, Container only)", - z.string().optional(), - ), - flag( - "instrumentation", - 'instrumentation config (JSON, e.g. {"enableOtel": true})', - z.string().optional(), - ), - flag( - "additional-policies", - "additional IAM policy ARNs or policy document paths", - z.array(z.string()).optional(), - ), ], handle: async (ctx, flags) => { if (!flags.name) throw new InputValidationError("required option '--name ' not specified"); - const inputArtifact = parseJsonFlag( - "agent-runtime-artifact", - flags["agent-runtime-artifact"], - ); + const sourceCount = [flags.template, flags["code-location"]].filter(Boolean).length; + if (sourceCount !== 1) + throw new InputValidationError("exactly one of --template or --code-location is required"); + const inputNetwork = parseJsonFlag( "network-configuration", flags["network-configuration"], @@ -145,47 +146,42 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => flags["environment-variables"], ); - const build = getBuildType(flags.build, inputArtifact); - const entrypoint = getEntrypoint(flags.entrypoint, inputArtifact); + const build = flags.build; + const entrypoint = flags.entrypoint; if (entrypoint && build === "Container") throw new InputValidationError( - `code entrypoint cannot be provided with Container build type`, + "code entrypoint cannot be provided with Container build type", ); + const network = toNetwork(inputNetwork); + if (flags["custom-docker-build-args"] && !flags.dockerfile && !flags["build-context-path"]) throw new InputValidationError( - " --custom-docker-build-args requires --dockerfile or --build-context-path", + "--custom-docker-build-args requires --dockerfile or --build-context-path", + ); + + if (flags["vpc-id"] && !network?.networkConfig) + throw new InputValidationError( + "--vpc-id requires --network-configuration with VPC network configuration", ); - const network = toNetwork(inputNetwork); const auth = toAuthorizer(inputAuthConfig); const protocol = toProtocol(inputProtocol); const requestHeaderAllowlist = toRequestHeaderAllowlist(inputRequestHeaders); const lifecycleConfiguration = toLifecycle(inputLifecycle); const filesystemConfigurations = toFilesystems(inputFilesystems); - const runtimeConfig = { + const infraConfig = { name: flags.name, description: flags.description, - build, - entrypoint, - runtimeVersion: getRuntimeVersion(inputArtifact), - dockerfile: flags.dockerfile, - buildContextPath: flags["build-context-path"], - customDockerBuildArgs: parseJsonFlag>( - "custom-docker-build-args", - flags["custom-docker-build-args"], - ), executionRoleArn: flags["role-arn"], additionalPolicies: flags["additional-policies"], - instrumentation: parseJsonFlag<{ enableOtel: boolean }>( - "instrumentation", - flags["instrumentation"], - ), envVars: toEnvironmentVariables(inputEnvironmentVariables), networkMode: network?.networkMode, - networkConfig: network?.networkConfig, + networkConfig: network?.networkConfig + ? { ...network.networkConfig, ...(flags["vpc-id"] ? { vpcId: flags["vpc-id"] } : {}) } + : undefined, authorizerType: auth?.authorizerType, authorizerConfiguration: auth?.authorizerConfiguration, protocol, @@ -195,6 +191,23 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => tags: parseJsonFlag>("tags", flags["tags"]), }; + const runtimeConfig = flags.template + ? { source: "template" as const, template: flags.template, ...infraConfig } + : { + source: "byo" as const, + codeLocation: flags["code-location"]!, + build: flags.build, + entrypoint, + runtimeVersion: flags["runtime-version"], + dockerfile: flags.dockerfile, + buildContextPath: flags["build-context-path"], + customDockerBuildArgs: parseJsonFlag>( + "custom-docker-build-args", + flags["custom-docker-build-args"], + ), + ...infraConfig, + }; + const project = ctx.require(ProjectKey); for await (const event of config.projectManager.addResource(project, { resourceType: "runtime", @@ -207,38 +220,6 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => }, }); -/** Converts AgentRuntimeArtifact union discriminator to project schema BuildType. */ -function getBuildType( - buildFlag: z.input | undefined, - runtimeArtifact: AgentRuntimeArtifact | undefined, -): BuildType { - if (runtimeArtifact && buildFlag) - throw new InputValidationError(`--runtime-artifact and --build are mutually exclusive`); - if (buildFlag) return buildFlag; - if (runtimeArtifact?.containerConfiguration) return "Container"; - if (runtimeArtifact?.codeConfiguration) return "CodeZip"; - throw new InputValidationError(`exactly one of --runtime-artifact and --build is required`); -} - -/** Converts codeConfiguration.entryPoint string[] to colon-joined "file.py:handler" format. */ -function getEntrypoint( - entrypointFlag: string | undefined, - runtimeArtifact: AgentRuntimeArtifact | undefined, -): string | undefined { - if (runtimeArtifact && entrypointFlag) - throw new InputValidationError(`--runtime-artifact and --entrypoint are mutually exclusive`); - if (entrypointFlag) return entrypointFlag; - if (runtimeArtifact?.codeConfiguration?.entryPoint) - return runtimeArtifact.codeConfiguration.entryPoint.join(":"); - return undefined; -} - -/** Converts codeConfiguration.runtime enum string to project schema RuntimeVersion. */ -function getRuntimeVersion(artifact: AgentRuntimeArtifact | undefined): RuntimeVersion | undefined { - if (!artifact?.codeConfiguration?.runtime) return undefined; - return artifact.codeConfiguration.runtime as RuntimeVersion; -} - /** Converts API flat {key: value} map to project schema [{name, value}] array. */ function toEnvironmentVariables(envVars: Record | undefined): EnvVar[] { return envVars ? Object.entries(envVars).map(([name, value]) => ({ name, value })) : []; @@ -299,7 +280,7 @@ function toRequestHeaderAllowlist( if ("requestHeaderAllowlist" in headers && headers.requestHeaderAllowlist) { return headers.requestHeaderAllowlist; } - return undefined; + throw new InputValidationError("Unrecognized request header configuration variant"); } /** Converts API LifecycleConfiguration to project schema LifecycleConfiguration. */ diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index ee479dac9..6fd6374ff 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -10,7 +10,7 @@ import { TestGlobalConfigAccessor, testIO, } from "../../testing"; -import { InputValidationError } from "../../errors"; +import { InputValidationError, NotImplementedError } from "../../errors"; import { FsReadWriteJson, type ReadWriteJson } from "../../io"; async function run(args: string[], opts?: { core?: TestCoreClient }) { @@ -112,7 +112,7 @@ describe("project create", () => { describe("project add harness", () => { const defaultModel = { provider: "bedrock", modelId: "global.anthropic.claude-sonnet-4-6" }; - /** Verify error case for different flags **/ + // Verifies each flag combination produces the expected harness config. test.each<[string, string[], Record]>([ ["minimal — name only", ["--name", "x"], { model: defaultModel }], [ @@ -610,6 +610,7 @@ describe("project add harness", () => { ); }); + // Rejects invalid flag combinations with InputValidationError. test.each([ ["missing --name", ["--model", '{"bedrockModelConfig":{"modelId":"x"}}']], ["model without modelId", ["--name", "x", "--model", '{"bedrockModelConfig":{}}']], @@ -715,3 +716,282 @@ describe("project build", () => { await expect(run(["build"])).rejects.toThrow(/npm install/); }); }); + +// TODO: Replace NotImplementedError assertions with output assertions once +// FsProjectManager.addResource supports the "runtime" resource type. +describe("project add runtime", () => { + const byo = ["--code-location", "app/my_agent"]; + const tpl = ["--template", "hello-world-python"]; + + // Verifies each valid flag combination passes handler validation. + test.each<[string, string[]]>([ + ["minimal — template path", ["--name", "my_agent", ...tpl]], + ["minimal — BYO path with build", ["--name", "my_agent", ...byo, "--build", "CodeZip"]], + [ + "BYO container with dockerfile", + ["--name", "my_agent", ...byo, "--build", "Container", "--dockerfile", "Dockerfile"], + ], + [ + "entrypoint + runtime-version for CodeZip", + [ + "--name", + "my_agent", + ...byo, + "--build", + "CodeZip", + "--entrypoint", + "app.py:main", + "--runtime-version", + "PYTHON_3_13", + ], + ], + ["description", ["--name", "my_agent", ...tpl, "--description", "A test agent"]], + [ + "role-arn", + ["--name", "my_agent", ...tpl, "--role-arn", "arn:aws:iam::123456789012:role/MyRole"], + ], + [ + "network-configuration — VPC", + [ + "--name", + "my_agent", + ...tpl, + "--network-configuration", + '{"networkMode":"VPC","networkModeConfig":{"subnets":["subnet-abc"],"securityGroups":["sg-123"]}}', + ], + ], + [ + "network-configuration — PUBLIC", + ["--name", "my_agent", ...tpl, "--network-configuration", '{"networkMode":"PUBLIC"}'], + ], + [ + "authorizer-configuration — customJWT", + [ + "--name", + "my_agent", + ...tpl, + "--authorizer-configuration", + '{"customJWTAuthorizer":{"discoveryUrl":"https://idp.example.com/.well-known/openid-configuration","allowedAudience":["app"]}}', + ], + ], + [ + "protocol-configuration — MCP", + ["--name", "my_agent", ...tpl, "--protocol-configuration", '{"serverProtocol":"MCP"}'], + ], + [ + "protocol-configuration — A2A", + ["--name", "my_agent", ...tpl, "--protocol-configuration", '{"serverProtocol":"A2A"}'], + ], + [ + "protocol-configuration — AGUI", + ["--name", "my_agent", ...tpl, "--protocol-configuration", '{"serverProtocol":"AGUI"}'], + ], + [ + "request-header-configuration", + [ + "--name", + "my_agent", + ...tpl, + "--request-header-configuration", + '{"requestHeaderAllowlist":["X-Custom-Header","Authorization"]}', + ], + ], + [ + "lifecycle-configuration", + [ + "--name", + "my_agent", + ...tpl, + "--lifecycle-configuration", + '{"idleRuntimeSessionTimeout":300,"maxLifetime":3600}', + ], + ], + [ + "environment-variables", + [ + "--name", + "my_agent", + ...tpl, + "--environment-variables", + '{"LOG_LEVEL":"debug","APP_ENV":"staging"}', + ], + ], + [ + "filesystem-configurations — sessionStorage", + [ + "--name", + "my_agent", + ...tpl, + "--filesystem-configurations", + '[{"sessionStorage":{"mountPath":"/mnt/data"}}]', + ], + ], + [ + "filesystem-configurations — efsAccessPoint", + [ + "--name", + "my_agent", + ...tpl, + "--filesystem-configurations", + '[{"efsAccessPoint":{"accessPointArn":"arn:aws:elasticfilesystem:us-east-1:123456789012:access-point/fsap-abc","mountPath":"/mnt/efs"}}]', + ], + ], + [ + "filesystem-configurations — s3FilesAccessPoint", + [ + "--name", + "my_agent", + ...tpl, + "--filesystem-configurations", + '[{"s3FilesAccessPoint":{"accessPointArn":"arn:aws:s3files:us-east-1:123456789012:file-system/fs-abc/access-point/fsap-def","mountPath":"/mnt/s3"}}]', + ], + ], + ["tags", ["--name", "my_agent", ...tpl, "--tags", '{"team":"ml","env":"prod"}']], + [ + "dockerfile + build-context-path", + [ + "--name", + "my_agent", + ...byo, + "--build", + "Container", + "--dockerfile", + "docker/Dockerfile.gpu", + "--build-context-path", + ".", + ], + ], + [ + "custom-docker-build-args with dockerfile", + [ + "--name", + "my_agent", + ...byo, + "--build", + "Container", + "--dockerfile", + "Dockerfile", + "--custom-docker-build-args", + '{"AGENT_NAME":"my_agent","VERSION":"1.0"}', + ], + ], + [ + "custom-docker-build-args with build-context-path", + [ + "--name", + "my_agent", + ...byo, + "--build", + "Container", + "--build-context-path", + ".", + "--custom-docker-build-args", + '{"AGENT_NAME":"my_agent"}', + ], + ], + [ + "additional-policies", + [ + "--name", + "my_agent", + ...tpl, + "--additional-policies", + "arn:aws:iam::123456789012:policy/MyPolicy", + ], + ], + [ + "vpc-id with VPC network configuration", + [ + "--name", + "my_agent", + ...byo, + "--build", + "Container", + "--dockerfile", + "Dockerfile", + "--network-configuration", + '{"networkMode":"VPC","networkModeConfig":{"subnets":["subnet-abc"],"securityGroups":["sg-123"]}}', + "--vpc-id", + "vpc-0123456789abcdef0", + ], + ], + ])("%s — accepts flags", async (_label, flags) => { + await inProject(); + await expect(run(["add", "runtime", ...flags])).rejects.toBeInstanceOf(NotImplementedError); + }); + + // Rejects invalid flag combinations with InputValidationError. + test.each<[string, string[]]>([ + ["missing --name", ["--template", "hello-world-python"]], + ["missing both --template and --code-location", ["--name", "my_agent"]], + [ + "--template and --code-location are mutually exclusive", + ["--name", "my_agent", "--template", "hello-world-python", "--code-location", "app/agent"], + ], + [ + "entrypoint cannot be provided with Container build type", + ["--name", "my_agent", ...byo, "--build", "Container", "--entrypoint", "main.py"], + ], + [ + "--custom-docker-build-args requires --dockerfile or --build-context-path", + [ + "--name", + "my_agent", + ...byo, + "--build", + "Container", + "--custom-docker-build-args", + '{"KEY":"value"}', + ], + ], + [ + "--vpc-id requires --network-configuration with VPC network configuration", + [ + "--name", + "my_agent", + ...byo, + "--build", + "Container", + "--dockerfile", + "Dockerfile", + "--vpc-id", + "vpc-0123456789abcdef0", + ], + ], + [ + "unrecognized authorizer configuration variant", + ["--name", "my_agent", ...tpl, "--authorizer-configuration", '{"unknownAuth":{}}'], + ], + [ + "missing discoveryUrl in authorizer", + [ + "--name", + "my_agent", + ...tpl, + "--authorizer-configuration", + '{"customJWTAuthorizer":{"allowedAudience":["a"]}}', + ], + ], + [ + "unrecognized filesystem configuration variant", + ["--name", "my_agent", ...tpl, "--filesystem-configurations", '[{"unknownFs":{}}]'], + ], + [ + "invalid JSON in --network-configuration", + ["--name", "my_agent", ...tpl, "--network-configuration", "{bad}"], + ], + [ + "unrecognized request header configuration variant", + [ + "--name", + "my_agent", + ...tpl, + "--request-header-configuration", + '{"unknownVariant":["X-Foo"]}', + ], + ], + ])("%s", async (_label, flags) => { + await inProject(); + await expect(run(["add", "runtime", ...flags])).rejects.toBeInstanceOf(InputValidationError); + }); +}); diff --git a/src/handlers/project/types.ts b/src/handlers/project/types.ts index 0f6692fea..771a86a69 100644 --- a/src/handlers/project/types.ts +++ b/src/handlers/project/types.ts @@ -1,14 +1,29 @@ import { HarnessSpecSchema } from "../../projectSchemas/harness"; import type { ProjectSpecSchema } from "../../projectSchemas/project"; import type z from "zod"; -import type { ProjectRuntimeSchema } from "../../projectSchemas/runtime"; +import type { + BuildType, + EnvVar, + FilesystemConfiguration, + LifecycleConfiguration, + NetworkConfig, +} from "../../projectSchemas/runtime"; +import type { AuthorizerConfig, RuntimeAuthorizerType } from "../../projectSchemas/auth"; +import type { NetworkMode, ProtocolMode, RuntimeVersion } from "../../projectSchemas/constants"; -/** Available project templates for scaffolding new AgentCore projects. */ -export const PROJECT_TEMPLATES = { +/** Available runtime templates for scaffolding agent code. A subset of {@link PROJECT_TEMPLATES} */ +export const RUNTIME_TEMPLATES = { HELLO_WORLD_PYTHON: "hello-world-python", HELLO_WORLD_PYTHON_CONTAINER: "hello-world-python-container", } as const; +export type RuntimeTemplate = (typeof RUNTIME_TEMPLATES)[keyof typeof RUNTIME_TEMPLATES]; + +/** Available project templates for scaffolding new AgentCore projects. */ +export const PROJECT_TEMPLATES = { + ...RUNTIME_TEMPLATES, +} as const; + export type ProjectTemplate = (typeof PROJECT_TEMPLATES)[keyof typeof PROJECT_TEMPLATES]; export type CreateProjectInput = { @@ -40,6 +55,44 @@ export type Project = { spec: z.infer; }; +/** Shared infrastructure config fields for a runtime (independent of source type). */ +type RuntimeInfraConfig = { + name: string; + description?: string; + executionRoleArn?: string; + additionalPolicies?: string[]; + envVars?: EnvVar[]; + networkMode?: NetworkMode; + networkConfig?: NetworkConfig; + authorizerType?: RuntimeAuthorizerType; + authorizerConfiguration?: AuthorizerConfig; + protocol?: ProtocolMode; + requestHeaderAllowlist?: string[]; + lifecycleConfiguration?: LifecycleConfiguration; + filesystemConfigurations?: FilesystemConfiguration[]; + tags?: Record; +}; + +/** BYO path: user provides existing code location and build config. */ +type RuntimeByoConfig = RuntimeInfraConfig & { + source: "byo"; + codeLocation: string; + build?: BuildType; + entrypoint?: string; + runtimeVersion?: RuntimeVersion; + dockerfile?: string; + buildContextPath?: string; + customDockerBuildArgs?: Record; +}; + +/** Template path: CLI scaffolds agent code from a template. */ +type RuntimeTemplateConfig = RuntimeInfraConfig & { + source: "template"; + template: string; +}; + +export type RuntimeResourceConfig = RuntimeByoConfig | RuntimeTemplateConfig; + /** Discriminated union input for {@link ProjectManager.addResource}. */ export type AddResourceInput = | { @@ -48,7 +101,7 @@ export type AddResourceInput = } | { resourceType: "runtime"; - resourceConfig: Omit, "codeLocation" | "runtimeVersion">; + resourceConfig: RuntimeResourceConfig; }; export type ProjectResource = AddResourceInput["resourceType"]; From 360dda2781120d93e4cf1990404e38e7cedc725a Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Tue, 18 Aug 2026 00:53:15 +0000 Subject: [PATCH 3/9] chore(runtime): undo entrypoint schema change, punt to follow-up --- src/handlers/project/add/runtime/index.ts | 9 ++------- src/handlers/project/project.test.ts | 4 ---- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/handlers/project/add/runtime/index.ts b/src/handlers/project/add/runtime/index.ts index ead37907c..ee1788bcf 100644 --- a/src/handlers/project/add/runtime/index.ts +++ b/src/handlers/project/add/runtime/index.ts @@ -146,13 +146,8 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => flags["environment-variables"], ); - const build = flags.build; - const entrypoint = flags.entrypoint; - - if (entrypoint && build === "Container") - throw new InputValidationError( - "code entrypoint cannot be provided with Container build type", - ); + // TODO: make entrypoint optional since container agents don't need it. + const entrypoint = flags.entrypoint ?? "main.py"; const network = toNetwork(inputNetwork); diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index 6fd6374ff..ce6947b87 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -928,10 +928,6 @@ describe("project add runtime", () => { "--template and --code-location are mutually exclusive", ["--name", "my_agent", "--template", "hello-world-python", "--code-location", "app/agent"], ], - [ - "entrypoint cannot be provided with Container build type", - ["--name", "my_agent", ...byo, "--build", "Container", "--entrypoint", "main.py"], - ], [ "--custom-docker-build-args requires --dockerfile or --build-context-path", [ From 559facfc20515805eb1559fc00dd10e445461b48 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Tue, 18 Aug 2026 14:01:03 +0000 Subject: [PATCH 4/9] refactor(runtime): clean up handler flags --- src/handlers/project/add/runtime/index.ts | 52 +++++++---------------- src/handlers/project/project.test.ts | 49 +++++++++++---------- src/handlers/project/types.ts | 4 +- src/projectSchemas/runtime.ts | 2 +- 4 files changed, 42 insertions(+), 65 deletions(-) diff --git a/src/handlers/project/add/runtime/index.ts b/src/handlers/project/add/runtime/index.ts index ee1788bcf..7b6b16ee3 100644 --- a/src/handlers/project/add/runtime/index.ts +++ b/src/handlers/project/add/runtime/index.ts @@ -14,26 +14,27 @@ import type { import { type EnvVar, type FilesystemConfiguration as ProjectFilesystemConfiguration, - type LifecycleConfiguration as ProjectLifecycleConfiguration, type NetworkConfig, BuildTypeSchema, } from "../../../../projectSchemas/runtime"; import type { AuthorizerConfig, RuntimeAuthorizerType } from "../../../../projectSchemas/auth"; -import { - type NetworkMode, - type ProtocolMode, - RuntimeVersionSchema, -} from "../../../../projectSchemas/constants"; +import { type NetworkMode, RuntimeVersionSchema } from "../../../../projectSchemas/constants"; import { RUNTIME_TEMPLATES } from "../../types"; export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => createHandler({ name: "runtime", - description: "adds a runtime to the current project either from a template or BYO", + description: + "adds a runtime to the current project either from a template or from existing local code", flags: [ flag("name", "the name of the runtime", z.string().optional()), - flag("description", "description of the runtime", z.string().optional()), - flag("template", "runtime template to scaffold from", z.enum(RUNTIME_TEMPLATES).optional()), + flag("description", "an optional description of the runtime", z.string().optional()), + flag("template", "template to scaffold from", z.enum(RUNTIME_TEMPLATES).optional()), + flag( + "role-arn", + "IAM role ARN that provides permissions for the runtime", + z.string().optional(), + ), flag("code-location", "path to existing agent source code (BYO path)", z.string().optional()), flag("build", "build type: CodeZip or Container (BYO only)", BuildTypeSchema.optional()), flag("entrypoint", "entrypoint file, e.g. main.py:handler (BYO only)", z.string().optional()), @@ -44,7 +45,7 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => ), flag( "dockerfile", - "dockerfile for the container build (BYO Container only)", + "dockerfile path for the container build (BYO Container only)", z.string().optional(), ), flag( @@ -57,14 +58,10 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => "docker build args as JSON key/value object (BYO Container only)", z.string().optional(), ), - flag( - "role-arn", - "IAM role ARN that provides permissions for the runtime", - z.string().optional(), - ), + flag( "additional-policies", - "additional IAM policy ARNs or policy document paths", + "additional IAM policy ARNs or policy document paths for the execution role", z.array(z.string()).optional(), ), flag( @@ -162,9 +159,7 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => ); const auth = toAuthorizer(inputAuthConfig); - const protocol = toProtocol(inputProtocol); const requestHeaderAllowlist = toRequestHeaderAllowlist(inputRequestHeaders); - const lifecycleConfiguration = toLifecycle(inputLifecycle); const filesystemConfigurations = toFilesystems(inputFilesystems); const infraConfig = { @@ -179,9 +174,9 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => : undefined, authorizerType: auth?.authorizerType, authorizerConfiguration: auth?.authorizerConfiguration, - protocol, + protocol: inputProtocol?.serverProtocol, requestHeaderAllowlist, - lifecycleConfiguration, + lifecycleConfiguration: inputLifecycle, filesystemConfigurations, tags: parseJsonFlag>("tags", flags["tags"]), }; @@ -261,12 +256,6 @@ function toAuthorizer( throw new InputValidationError("Unrecognized authorizer configuration variant"); } -/** Converts API ProtocolConfiguration wrapper to project schema ProtocolMode enum. */ -function toProtocol(protocol: ProtocolConfiguration | undefined): ProtocolMode | undefined { - if (!protocol) return undefined; - return protocol.serverProtocol as ProtocolMode; -} - /** Unwraps API RequestHeaderConfiguration union to project schema string[]. */ function toRequestHeaderAllowlist( headers: RequestHeaderConfiguration | undefined, @@ -278,17 +267,6 @@ function toRequestHeaderAllowlist( throw new InputValidationError("Unrecognized request header configuration variant"); } -/** Converts API LifecycleConfiguration to project schema LifecycleConfiguration. */ -function toLifecycle( - lifecycle: LifecycleConfiguration | undefined, -): ProjectLifecycleConfiguration | undefined { - if (!lifecycle) return undefined; - return { - idleRuntimeSessionTimeout: lifecycle.idleRuntimeSessionTimeout, - maxLifetime: lifecycle.maxLifetime, - }; -} - /** Converts API FilesystemConfiguration[] tagged unions to project schema format. */ function toFilesystems( filesystems: FilesystemConfiguration[] | undefined, diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index ce6947b87..2c5dff4ba 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -112,7 +112,7 @@ describe("project create", () => { describe("project add harness", () => { const defaultModel = { provider: "bedrock", modelId: "global.anthropic.claude-sonnet-4-6" }; - // Verifies each flag combination produces the expected harness config. + /** Verify error case for different flags **/ test.each<[string, string[], Record]>([ ["minimal — name only", ["--name", "x"], { model: defaultModel }], [ @@ -610,7 +610,6 @@ describe("project add harness", () => { ); }); - // Rejects invalid flag combinations with InputValidationError. test.each([ ["missing --name", ["--model", '{"bedrockModelConfig":{"modelId":"x"}}']], ["model without modelId", ["--name", "x", "--model", '{"bedrockModelConfig":{}}']], @@ -721,11 +720,11 @@ describe("project build", () => { // FsProjectManager.addResource supports the "runtime" resource type. describe("project add runtime", () => { const byo = ["--code-location", "app/my_agent"]; - const tpl = ["--template", "hello-world-python"]; + const template = ["--template", "hello-world-python"]; // Verifies each valid flag combination passes handler validation. test.each<[string, string[]]>([ - ["minimal — template path", ["--name", "my_agent", ...tpl]], + ["minimal — template path", ["--name", "my_agent", ...template]], ["minimal — BYO path with build", ["--name", "my_agent", ...byo, "--build", "CodeZip"]], [ "BYO container with dockerfile", @@ -745,53 +744,53 @@ describe("project add runtime", () => { "PYTHON_3_13", ], ], - ["description", ["--name", "my_agent", ...tpl, "--description", "A test agent"]], + ["description", ["--name", "my_agent", ...template, "--description", "A test agent"]], [ "role-arn", - ["--name", "my_agent", ...tpl, "--role-arn", "arn:aws:iam::123456789012:role/MyRole"], + ["--name", "my_agent", ...template, "--role-arn", "arn:aws:iam::123456789012:role/MyRole"], ], [ "network-configuration — VPC", [ "--name", "my_agent", - ...tpl, + ...template, "--network-configuration", '{"networkMode":"VPC","networkModeConfig":{"subnets":["subnet-abc"],"securityGroups":["sg-123"]}}', ], ], [ "network-configuration — PUBLIC", - ["--name", "my_agent", ...tpl, "--network-configuration", '{"networkMode":"PUBLIC"}'], + ["--name", "my_agent", ...template, "--network-configuration", '{"networkMode":"PUBLIC"}'], ], [ "authorizer-configuration — customJWT", [ "--name", "my_agent", - ...tpl, + ...template, "--authorizer-configuration", '{"customJWTAuthorizer":{"discoveryUrl":"https://idp.example.com/.well-known/openid-configuration","allowedAudience":["app"]}}', ], ], [ "protocol-configuration — MCP", - ["--name", "my_agent", ...tpl, "--protocol-configuration", '{"serverProtocol":"MCP"}'], + ["--name", "my_agent", ...template, "--protocol-configuration", '{"serverProtocol":"MCP"}'], ], [ "protocol-configuration — A2A", - ["--name", "my_agent", ...tpl, "--protocol-configuration", '{"serverProtocol":"A2A"}'], + ["--name", "my_agent", ...template, "--protocol-configuration", '{"serverProtocol":"A2A"}'], ], [ "protocol-configuration — AGUI", - ["--name", "my_agent", ...tpl, "--protocol-configuration", '{"serverProtocol":"AGUI"}'], + ["--name", "my_agent", ...template, "--protocol-configuration", '{"serverProtocol":"AGUI"}'], ], [ "request-header-configuration", [ "--name", "my_agent", - ...tpl, + ...template, "--request-header-configuration", '{"requestHeaderAllowlist":["X-Custom-Header","Authorization"]}', ], @@ -801,7 +800,7 @@ describe("project add runtime", () => { [ "--name", "my_agent", - ...tpl, + ...template, "--lifecycle-configuration", '{"idleRuntimeSessionTimeout":300,"maxLifetime":3600}', ], @@ -811,7 +810,7 @@ describe("project add runtime", () => { [ "--name", "my_agent", - ...tpl, + ...template, "--environment-variables", '{"LOG_LEVEL":"debug","APP_ENV":"staging"}', ], @@ -821,7 +820,7 @@ describe("project add runtime", () => { [ "--name", "my_agent", - ...tpl, + ...template, "--filesystem-configurations", '[{"sessionStorage":{"mountPath":"/mnt/data"}}]', ], @@ -831,7 +830,7 @@ describe("project add runtime", () => { [ "--name", "my_agent", - ...tpl, + ...template, "--filesystem-configurations", '[{"efsAccessPoint":{"accessPointArn":"arn:aws:elasticfilesystem:us-east-1:123456789012:access-point/fsap-abc","mountPath":"/mnt/efs"}}]', ], @@ -841,12 +840,12 @@ describe("project add runtime", () => { [ "--name", "my_agent", - ...tpl, + ...template, "--filesystem-configurations", '[{"s3FilesAccessPoint":{"accessPointArn":"arn:aws:s3files:us-east-1:123456789012:file-system/fs-abc/access-point/fsap-def","mountPath":"/mnt/s3"}}]', ], ], - ["tags", ["--name", "my_agent", ...tpl, "--tags", '{"team":"ml","env":"prod"}']], + ["tags", ["--name", "my_agent", ...template, "--tags", '{"team":"ml","env":"prod"}']], [ "dockerfile + build-context-path", [ @@ -894,7 +893,7 @@ describe("project add runtime", () => { [ "--name", "my_agent", - ...tpl, + ...template, "--additional-policies", "arn:aws:iam::123456789012:policy/MyPolicy", ], @@ -956,32 +955,32 @@ describe("project add runtime", () => { ], [ "unrecognized authorizer configuration variant", - ["--name", "my_agent", ...tpl, "--authorizer-configuration", '{"unknownAuth":{}}'], + ["--name", "my_agent", ...template, "--authorizer-configuration", '{"unknownAuth":{}}'], ], [ "missing discoveryUrl in authorizer", [ "--name", "my_agent", - ...tpl, + ...template, "--authorizer-configuration", '{"customJWTAuthorizer":{"allowedAudience":["a"]}}', ], ], [ "unrecognized filesystem configuration variant", - ["--name", "my_agent", ...tpl, "--filesystem-configurations", '[{"unknownFs":{}}]'], + ["--name", "my_agent", ...template, "--filesystem-configurations", '[{"unknownFs":{}}]'], ], [ "invalid JSON in --network-configuration", - ["--name", "my_agent", ...tpl, "--network-configuration", "{bad}"], + ["--name", "my_agent", ...template, "--network-configuration", "{bad}"], ], [ "unrecognized request header configuration variant", [ "--name", "my_agent", - ...tpl, + ...template, "--request-header-configuration", '{"unknownVariant":["X-Foo"]}', ], diff --git a/src/handlers/project/types.ts b/src/handlers/project/types.ts index 771a86a69..be8952fbf 100644 --- a/src/handlers/project/types.ts +++ b/src/handlers/project/types.ts @@ -11,7 +11,7 @@ import type { import type { AuthorizerConfig, RuntimeAuthorizerType } from "../../projectSchemas/auth"; import type { NetworkMode, ProtocolMode, RuntimeVersion } from "../../projectSchemas/constants"; -/** Available runtime templates for scaffolding agent code. A subset of {@link PROJECT_TEMPLATES} */ +/** Available runtime templates for scaffolding agent code. A subset of {@link PROJECT_TEMPLATES} describing runtimes only */ export const RUNTIME_TEMPLATES = { HELLO_WORLD_PYTHON: "hello-world-python", HELLO_WORLD_PYTHON_CONTAINER: "hello-world-python-container", @@ -88,7 +88,7 @@ type RuntimeByoConfig = RuntimeInfraConfig & { /** Template path: CLI scaffolds agent code from a template. */ type RuntimeTemplateConfig = RuntimeInfraConfig & { source: "template"; - template: string; + template: RuntimeTemplate; }; export type RuntimeResourceConfig = RuntimeByoConfig | RuntimeTemplateConfig; diff --git a/src/projectSchemas/runtime.ts b/src/projectSchemas/runtime.ts index 33cb295da..aba8d291b 100644 --- a/src/projectSchemas/runtime.ts +++ b/src/projectSchemas/runtime.ts @@ -241,7 +241,7 @@ export const ProjectRuntimeSchema = z name: AgentNameSchema, description: z.string().max(200).optional(), build: BuildTypeSchema, - entrypoint: EntrypointSchema.optional(), + entrypoint: EntrypointSchema, codeLocation: DirectoryPathSchema, dockerfile: DockerfilePathSchema.optional(), buildContextPath: DirectoryPathSchema.optional(), From a51a3a6a5265b612018cbd2f86514515f25c8239 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Tue, 18 Aug 2026 14:08:43 +0000 Subject: [PATCH 5/9] feat(runtime): default to hello world python --- src/handlers/project/add/runtime/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/handlers/project/add/runtime/index.ts b/src/handlers/project/add/runtime/index.ts index 7b6b16ee3..318d3b188 100644 --- a/src/handlers/project/add/runtime/index.ts +++ b/src/handlers/project/add/runtime/index.ts @@ -29,7 +29,11 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => flags: [ flag("name", "the name of the runtime", z.string().optional()), flag("description", "an optional description of the runtime", z.string().optional()), - flag("template", "template to scaffold from", z.enum(RUNTIME_TEMPLATES).optional()), + flag( + "template", + "template to scaffold from", + z.enum(RUNTIME_TEMPLATES).default(RUNTIME_TEMPLATES.HELLO_WORLD_PYTHON), + ), flag( "role-arn", "IAM role ARN that provides permissions for the runtime", From 4adff645ad82bbf8b3fd3b6081514477cc7c1104 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Tue, 18 Aug 2026 14:14:51 +0000 Subject: [PATCH 6/9] fix(runtime): remove dead space --- src/handlers/project/add/runtime/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/handlers/project/add/runtime/index.ts b/src/handlers/project/add/runtime/index.ts index 318d3b188..0d1eb403f 100644 --- a/src/handlers/project/add/runtime/index.ts +++ b/src/handlers/project/add/runtime/index.ts @@ -62,7 +62,6 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => "docker build args as JSON key/value object (BYO Container only)", z.string().optional(), ), - flag( "additional-policies", "additional IAM policy ARNs or policy document paths for the execution role", From f97e5127a07a375ed2e8f579027f84336846392d Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Tue, 18 Aug 2026 19:00:07 +0000 Subject: [PATCH 7/9] feat(runtime): bring parity with old command --- src/handlers/project/add/runtime/index.ts | 97 ++++++++++++++++++++--- src/handlers/project/project.test.ts | 75 ++++++++++++++++++ src/handlers/project/types.ts | 27 ++++++- 3 files changed, 187 insertions(+), 12 deletions(-) diff --git a/src/handlers/project/add/runtime/index.ts b/src/handlers/project/add/runtime/index.ts index 0d1eb403f..ef3ab1c62 100644 --- a/src/handlers/project/add/runtime/index.ts +++ b/src/handlers/project/add/runtime/index.ts @@ -3,13 +3,14 @@ import { createHandler, flag, ProjectKey } from "../../../../router"; import type { AddProjectResourceConfig } from "../types"; import { parseJsonFlag } from "../../../utils"; import { InputValidationError } from "../../../../errors"; -import type { - AuthorizerConfiguration, - FilesystemConfiguration, - LifecycleConfiguration, - NetworkConfiguration, - ProtocolConfiguration, - RequestHeaderConfiguration, +import { + ServerProtocol, + type AuthorizerConfiguration, + type FilesystemConfiguration, + type LifecycleConfiguration, + type NetworkConfiguration, + type ProtocolConfiguration, + type RequestHeaderConfiguration, } from "@aws-sdk/client-bedrock-agentcore-control"; import { type EnvVar, @@ -19,7 +20,12 @@ import { } from "../../../../projectSchemas/runtime"; import type { AuthorizerConfig, RuntimeAuthorizerType } from "../../../../projectSchemas/auth"; import { type NetworkMode, RuntimeVersionSchema } from "../../../../projectSchemas/constants"; -import { RUNTIME_TEMPLATES } from "../../types"; +import { + runtimeModelProviderSchema, + RUNTIME_TEMPLATES, + runtimeMemoryConfigSchema, +} from "../../types"; +import { SourceResolver } from "../../../../io"; export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => createHandler({ @@ -40,8 +46,23 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => z.string().optional(), ), flag("code-location", "path to existing agent source code (BYO path)", z.string().optional()), - flag("build", "build type: CodeZip or Container (BYO only)", BuildTypeSchema.optional()), + flag("build", "build type: CodeZip or Container", BuildTypeSchema.optional()), flag("entrypoint", "entrypoint file, e.g. main.py:handler (BYO only)", z.string().optional()), + flag( + "protocol", + "remote server protocol for the runtime (ex. http, mcp, a2a, etc.) shorthand for --protocol-configuration", + z.string().optional(), + ), + flag( + "api-key", + "API key source for non-bedrock model providers: '-' for stdin, 'file://path' for file", + z.string().optional(), + ), + flag( + "model-provider", + "model provider (template only)", + runtimeModelProviderSchema.optional(), + ), flag( "runtime-version", "language runtime, e.g. PYTHON_3_13, NODE_22 (BYO CodeZip only)", @@ -107,6 +128,11 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => "filesystem mount configurations (JSON FilesystemConfiguration[])", z.string().optional(), ), + flag( + "memory", + "memory configuration (JSON with mode: none | create | existing ) (template only)", + z.string().optional(), + ), flag("tags", "tags to apply (JSON object of key/value strings)", z.string().optional()), ], handle: async (ctx, flags) => { @@ -117,6 +143,29 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => if (sourceCount !== 1) throw new InputValidationError("exactly one of --template or --code-location is required"); + const isTemplate = Boolean(flags.template); + const templateOnlyFlags = (["memory", "model-provider", "api-key"] as const).filter( + (f) => flags[f], + ); + const byoOnlyFlags = ( + [ + "entrypoint", + "runtime-version", + "dockerfile", + "build-context-path", + "custom-docker-build-args", + ] as const + ).filter((f) => flags[f]); + + if (isTemplate && byoOnlyFlags.length > 0) + throw new InputValidationError( + `--${byoOnlyFlags[0]} is only available on the BYO path (--code-location)`, + ); + if (!isTemplate && templateOnlyFlags.length > 0) + throw new InputValidationError( + `--${templateOnlyFlags[0]} is only available on the template path (--template)`, + ); + const inputNetwork = parseJsonFlag( "network-configuration", flags["network-configuration"], @@ -145,12 +194,16 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => "environment-variables", flags["environment-variables"], ); + const memoryConfiguration = parseMemoryConfig(flags["memory"]); // TODO: make entrypoint optional since container agents don't need it. const entrypoint = flags.entrypoint ?? "main.py"; const network = toNetwork(inputNetwork); + const source = new SourceResolver({ stdin: config.io.stdin }); + const apiKey = await source.resolveText("api-key", flags["api-key"]); + if (flags["custom-docker-build-args"] && !flags.dockerfile && !flags["build-context-path"]) throw new InputValidationError( "--custom-docker-build-args requires --dockerfile or --build-context-path", @@ -161,6 +214,11 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => "--vpc-id requires --network-configuration with VPC network configuration", ); + if (flags["protocol"] && flags["protocol-configuration"]) + throw new InputValidationError( + "--protocol and --protocol-configuration are mutually exclusive", + ); + const auth = toAuthorizer(inputAuthConfig); const requestHeaderAllowlist = toRequestHeaderAllowlist(inputRequestHeaders); const filesystemConfigurations = toFilesystems(inputFilesystems); @@ -177,7 +235,7 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => : undefined, authorizerType: auth?.authorizerType, authorizerConfiguration: auth?.authorizerConfiguration, - protocol: inputProtocol?.serverProtocol, + protocol: (flags["protocol"] as ServerProtocol) ?? inputProtocol?.serverProtocol, requestHeaderAllowlist, lifecycleConfiguration: inputLifecycle, filesystemConfigurations, @@ -185,7 +243,13 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => }; const runtimeConfig = flags.template - ? { source: "template" as const, template: flags.template, ...infraConfig } + ? { + source: "template" as const, + template: flags.template, + memory: memoryConfiguration, + modelProvider: { apiKey, provider: flags["model-provider"] }, + ...infraConfig, + } : { source: "byo" as const, codeLocation: flags["code-location"]!, @@ -213,6 +277,17 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => }, }); +/** Parses and validates the --memory JSON flag against the runtime memory config schema. */ +function parseMemoryConfig( + raw: string | undefined, +): z.infer | undefined { + if (!raw) return undefined; + const parsed = parseJsonFlag>("memory", raw); + const result = runtimeMemoryConfigSchema.safeParse(parsed); + if (!result.success) throw new InputValidationError(z.prettifyError(result.error)); + return result.data; +} + /** Converts API flat {key: value} map to project schema [{name, value}] array. */ function toEnvironmentVariables(envVars: Record | undefined): EnvVar[] { return envVars ? Object.entries(envVars).map(([name, value]) => ({ name, value })) : []; diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index 2c5dff4ba..389754afb 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -898,6 +898,33 @@ describe("project add runtime", () => { "arn:aws:iam::123456789012:policy/MyPolicy", ], ], + ["protocol shortcut", ["--name", "my_agent", ...template, "--protocol", "MCP"]], + [ + "memory — create with strategies", + [ + "--name", + "my_agent", + ...template, + "--memory", + '{"mode":"create","strategies":["SEMANTIC","EPISODIC"]}', + ], + ], + [ + "memory — existing by ARN", + [ + "--name", + "my_agent", + ...template, + "--memory", + '{"mode":"existing","arn":"arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/MyMem"}', + ], + ], + ["memory — disabled", ["--name", "my_agent", ...template, "--memory", '{"mode":"disabled"}']], + ["model-provider — openai", ["--name", "my_agent", ...template, "--model-provider", "openai"]], + [ + "build on template path (overlay)", + ["--name", "my_agent", ...template, "--build", "Container"], + ], [ "vpc-id with VPC network configuration", [ @@ -985,6 +1012,54 @@ describe("project add runtime", () => { '{"unknownVariant":["X-Foo"]}', ], ], + [ + "--protocol and --protocol-configuration are mutually exclusive", + [ + "--name", + "my_agent", + ...template, + "--protocol", + "MCP", + "--protocol-configuration", + '{"serverProtocol":"MCP"}', + ], + ], + [ + "--entrypoint is only available on BYO path", + ["--name", "my_agent", ...template, "--entrypoint", "main.py"], + ], + [ + "--runtime-version is only available on BYO path", + ["--name", "my_agent", ...template, "--runtime-version", "PYTHON_3_13"], + ], + [ + "--dockerfile is only available on BYO path", + ["--name", "my_agent", ...template, "--dockerfile", "Dockerfile"], + ], + [ + "--build-context-path is only available on BYO path", + ["--name", "my_agent", ...template, "--build-context-path", "."], + ], + [ + "--custom-docker-build-args is only available on BYO path", + ["--name", "my_agent", ...template, "--custom-docker-build-args", '{"KEY":"val"}'], + ], + [ + "--memory is only available on template path", + ["--name", "my_agent", ...byo, "--memory", '{"mode":"disabled"}'], + ], + [ + "--model-provider is only available on template path", + ["--name", "my_agent", ...byo, "--model-provider", "openai"], + ], + [ + "--api-key is only available on template path", + ["--name", "my_agent", ...byo, "--api-key", "-"], + ], + [ + "invalid memory JSON schema", + ["--name", "my_agent", ...template, "--memory", '{"mode":"invalid"}'], + ], ])("%s", async (_label, flags) => { await inProject(); await expect(run(["add", "runtime", ...flags])).rejects.toBeInstanceOf(InputValidationError); diff --git a/src/handlers/project/types.ts b/src/handlers/project/types.ts index be8952fbf..79a43c7ff 100644 --- a/src/handlers/project/types.ts +++ b/src/handlers/project/types.ts @@ -1,6 +1,6 @@ import { HarnessSpecSchema } from "../../projectSchemas/harness"; import type { ProjectSpecSchema } from "../../projectSchemas/project"; -import type z from "zod"; +import z from "zod"; import type { BuildType, EnvVar, @@ -10,6 +10,7 @@ import type { } from "../../projectSchemas/runtime"; import type { AuthorizerConfig, RuntimeAuthorizerType } from "../../projectSchemas/auth"; import type { NetworkMode, ProtocolMode, RuntimeVersion } from "../../projectSchemas/constants"; +import { MemoryStrategyType } from "@aws-sdk/client-bedrock-agentcore-control"; /** Available runtime templates for scaffolding agent code. A subset of {@link PROJECT_TEMPLATES} describing runtimes only */ export const RUNTIME_TEMPLATES = { @@ -89,6 +90,8 @@ type RuntimeByoConfig = RuntimeInfraConfig & { type RuntimeTemplateConfig = RuntimeInfraConfig & { source: "template"; template: RuntimeTemplate; + memory?: RuntimeMemoryConfig; + modelProvider?: RuntimeModelProviderConfig; }; export type RuntimeResourceConfig = RuntimeByoConfig | RuntimeTemplateConfig; @@ -122,3 +125,25 @@ export interface ProjectManager { /** Add a resource to an existing AgentCore project. */ addResource(project: Project, input: AddResourceInput): AsyncGenerator; } + +export const runtimeModelProviderSchema = z.enum(["bedrock", "anthropic", "openai", "gemini"]); +export type RuntimeModelProvider = z.infer; + +export type RuntimeModelProviderConfig = { + provider?: RuntimeModelProvider; + apiKey?: string; +}; + +export const runtimeMemoryConfigSchema = z.discriminatedUnion("mode", [ + z.object({ mode: z.literal("disabled") }), + z.object({ + mode: z.literal("create"), + strategies: z.array(z.enum(Object.values(MemoryStrategyType))), + }), + z.object({ + mode: z.literal("existing"), + arn: z.string().min(1), + }), +]); + +export type RuntimeMemoryConfig = z.input; From 0ad1145bf3868036a58eb7e35e1092c402d65e0a Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Tue, 18 Aug 2026 19:18:59 +0000 Subject: [PATCH 8/9] fix(runtime): default runtime to hello world template --- src/handlers/project/add/runtime/index.ts | 18 +++++++----------- src/handlers/project/project.test.ts | 4 ++-- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/handlers/project/add/runtime/index.ts b/src/handlers/project/add/runtime/index.ts index ef3ab1c62..48d3cc19e 100644 --- a/src/handlers/project/add/runtime/index.ts +++ b/src/handlers/project/add/runtime/index.ts @@ -35,11 +35,7 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => flags: [ flag("name", "the name of the runtime", z.string().optional()), flag("description", "an optional description of the runtime", z.string().optional()), - flag( - "template", - "template to scaffold from", - z.enum(RUNTIME_TEMPLATES).default(RUNTIME_TEMPLATES.HELLO_WORLD_PYTHON), - ), + flag("template", "template to scaffold from", z.enum(RUNTIME_TEMPLATES).optional()), flag( "role-arn", "IAM role ARN that provides permissions for the runtime", @@ -139,11 +135,11 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => if (!flags.name) throw new InputValidationError("required option '--name ' not specified"); - const sourceCount = [flags.template, flags["code-location"]].filter(Boolean).length; - if (sourceCount !== 1) - throw new InputValidationError("exactly one of --template or --code-location is required"); + if (flags.template && flags["code-location"]) + throw new InputValidationError("--template and --code-location are mutually exclusive"); - const isTemplate = Boolean(flags.template); + const isTemplate = !flags["code-location"]; + const template = flags.template ?? RUNTIME_TEMPLATES.HELLO_WORLD_PYTHON; const templateOnlyFlags = (["memory", "model-provider", "api-key"] as const).filter( (f) => flags[f], ); @@ -242,10 +238,10 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => tags: parseJsonFlag>("tags", flags["tags"]), }; - const runtimeConfig = flags.template + const runtimeConfig = isTemplate ? { source: "template" as const, - template: flags.template, + template, memory: memoryConfiguration, modelProvider: { apiKey, provider: flags["model-provider"] }, ...infraConfig, diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index 389754afb..d5b4a63dc 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -724,7 +724,8 @@ describe("project add runtime", () => { // Verifies each valid flag combination passes handler validation. test.each<[string, string[]]>([ - ["minimal — template path", ["--name", "my_agent", ...template]], + ["minimal — name only (defaults to template)", ["--name", "my_agent"]], + ["explicit template path", ["--name", "my_agent", ...template]], ["minimal — BYO path with build", ["--name", "my_agent", ...byo, "--build", "CodeZip"]], [ "BYO container with dockerfile", @@ -949,7 +950,6 @@ describe("project add runtime", () => { // Rejects invalid flag combinations with InputValidationError. test.each<[string, string[]]>([ ["missing --name", ["--template", "hello-world-python"]], - ["missing both --template and --code-location", ["--name", "my_agent"]], [ "--template and --code-location are mutually exclusive", ["--name", "my_agent", "--template", "hello-world-python", "--code-location", "app/agent"], From ef9de8d82898cd223777c88bb4272a0c9011c890 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Tue, 18 Aug 2026 19:24:18 +0000 Subject: [PATCH 9/9] fix(runtime): strongly type protocol --- src/handlers/project/add/runtime/index.ts | 29 +++++++++++------------ 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/handlers/project/add/runtime/index.ts b/src/handlers/project/add/runtime/index.ts index 48d3cc19e..f26779760 100644 --- a/src/handlers/project/add/runtime/index.ts +++ b/src/handlers/project/add/runtime/index.ts @@ -3,14 +3,13 @@ import { createHandler, flag, ProjectKey } from "../../../../router"; import type { AddProjectResourceConfig } from "../types"; import { parseJsonFlag } from "../../../utils"; import { InputValidationError } from "../../../../errors"; -import { - ServerProtocol, - type AuthorizerConfiguration, - type FilesystemConfiguration, - type LifecycleConfiguration, - type NetworkConfiguration, - type ProtocolConfiguration, - type RequestHeaderConfiguration, +import type { + AuthorizerConfiguration, + FilesystemConfiguration, + LifecycleConfiguration, + NetworkConfiguration, + ProtocolConfiguration, + RequestHeaderConfiguration, } from "@aws-sdk/client-bedrock-agentcore-control"; import { type EnvVar, @@ -19,7 +18,11 @@ import { BuildTypeSchema, } from "../../../../projectSchemas/runtime"; import type { AuthorizerConfig, RuntimeAuthorizerType } from "../../../../projectSchemas/auth"; -import { type NetworkMode, RuntimeVersionSchema } from "../../../../projectSchemas/constants"; +import { + type NetworkMode, + ProtocolModeSchema, + RuntimeVersionSchema, +} from "../../../../projectSchemas/constants"; import { runtimeModelProviderSchema, RUNTIME_TEMPLATES, @@ -44,11 +47,7 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => flag("code-location", "path to existing agent source code (BYO path)", z.string().optional()), flag("build", "build type: CodeZip or Container", BuildTypeSchema.optional()), flag("entrypoint", "entrypoint file, e.g. main.py:handler (BYO only)", z.string().optional()), - flag( - "protocol", - "remote server protocol for the runtime (ex. http, mcp, a2a, etc.) shorthand for --protocol-configuration", - z.string().optional(), - ), + flag("protocol", "server protocol ex. HTTP, MCP, A2A, AGUI", ProtocolModeSchema.optional()), flag( "api-key", "API key source for non-bedrock model providers: '-' for stdin, 'file://path' for file", @@ -231,7 +230,7 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => : undefined, authorizerType: auth?.authorizerType, authorizerConfiguration: auth?.authorizerConfiguration, - protocol: (flags["protocol"] as ServerProtocol) ?? inputProtocol?.serverProtocol, + protocol: flags["protocol"] ?? inputProtocol?.serverProtocol, requestHeaderAllowlist, lifecycleConfiguration: inputLifecycle, filesystemConfigurations,