diff --git a/src/core/project/manager.tsx b/src/core/project/manager.tsx index c6a8dc25f..05942496a 100644 --- a/src/core/project/manager.tsx +++ b/src/core/project/manager.tsx @@ -184,16 +184,15 @@ export class FsProjectManager implements ProjectManager { yield { message: `Updating project spec file at '${agentCoreSpecPath}'` }; - const newSpec = { ...existingProjectSpec, [projectSpecKey]: newResources }; - const newSpecParseResult = ProjectSpecSchema.safeParse(newSpec); - - if (!newSpecParseResult.success) - throw new InputValidationError(z.prettifyError(newSpecParseResult.error), { - cause: newSpecParseResult.error, - }); - // rollback scaffolding changes on failed config writes to prevent bad state. try { + const newSpec = { ...existingProjectSpec, [projectSpecKey]: newResources }; + const newSpecParseResult = ProjectSpecSchema.safeParse(newSpec); + + if (!newSpecParseResult.success) + throw new InputValidationError(z.prettifyError(newSpecParseResult.error), { + cause: newSpecParseResult.error, + }); const newProjectSpec = await this.json.write(agentCoreSpecPath, newSpecParseResult.data); return { diff --git a/src/handlers/project/add/index.ts b/src/handlers/project/add/index.ts index 50a7e1a2f..9f8858e31 100644 --- a/src/handlers/project/add/index.ts +++ b/src/handlers/project/add/index.ts @@ -2,6 +2,7 @@ import { withProject } from "../../../middleware/"; import { Router } from "../../../router"; import { createAddConfigBundleHandler } from "./config-bundle"; import { createAddHarnessHandler } from "./harness"; +import { createAddRuntimeHandler } from "./runtime"; import { createAddOnlineEvalHandler } from "./online-eval"; import type { AddProjectResourceConfig } from "./types"; @@ -10,6 +11,7 @@ export function createAddProjectResourceHandler(config: AddProjectResourceConfig projectAdd.use(withProject({ projectManager: config.projectManager, cwd: process.cwd() })); projectAdd.handler(createAddConfigBundleHandler(config)); projectAdd.handler(createAddHarnessHandler(config)); + projectAdd.handler(createAddRuntimeHandler(config)); projectAdd.handler(createAddOnlineEvalHandler(config)); return projectAdd; } diff --git a/src/handlers/project/add/runtime/index.test.ts b/src/handlers/project/add/runtime/index.test.ts new file mode 100644 index 000000000..adc4df843 --- /dev/null +++ b/src/handlers/project/add/runtime/index.test.ts @@ -0,0 +1,336 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { mkdtemp, rm } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { createRootHandler } from "../../../index"; +import { + createSilentLogger, + TestCoreClient, + TestGlobalConfigAccessor, + testIO, +} from "../../../../testing"; +import { InputValidationError, NotImplementedError } from "../../../../errors"; + +const originalCwd = process.cwd(); +const tempDirectories: string[] = []; + +async function inTempDirectory(): Promise { + const directory = await mkdtemp(join(tmpdir(), "agentcore-runtime-")); + tempDirectories.push(directory); + process.chdir(directory); + return process.cwd(); +} + +afterEach(async () => { + process.chdir(originalCwd); + await Promise.all( + tempDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })), + ); +}); + +async function run(args: string[]) { + const io = testIO(); + const core = new TestCoreClient(); + const root = createRootHandler(core, { + io: io.io, + globalConfigAccessor: new TestGlobalConfigAccessor(), + logger: createSilentLogger(), + }); + await root.route(["node", "agentcore", "project", ...args]); + return { io, core }; +} + +async function inProject(name = "TestProject"): Promise { + const directory = await inTempDirectory(); + await run(["create", "--name", name, "--skip-install", "--skip-git"]); + const projectRoot = join(directory, name); + process.chdir(projectRoot); + return projectRoot; +} + +// 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 template = ["--template", "hello-world-python"]; + + test.each<[string, string[]]>([ + ["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", + ["--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", ...template, "--description", "A test agent"]], + [ + "role-arn", + ["--name", "my_agent", ...template, "--role-arn", "arn:aws:iam::123456789012:role/MyRole"], + ], + [ + "network — VPC", + [ + "--name", + "my_agent", + ...template, + "--network-mode", + "VPC", + "--network-config", + '{"subnets":["subnet-0123456789abcdef0"],"securityGroups":["sg-0123456789abcdef0"]}', + ], + ], + ["network — PUBLIC", ["--name", "my_agent", ...template, "--network-mode", "PUBLIC"]], + [ + "authorizer — customJWT", + [ + "--name", + "my_agent", + ...template, + "--authorizer-type", + "CUSTOM_JWT", + "--authorizer-configuration", + '{"customJwtAuthorizer":{"discoveryUrl":"https://idp.example.com/.well-known/openid-configuration","allowedAudience":["app"]}}', + ], + ], + ["protocol — MCP", ["--name", "my_agent", ...template, "--protocol", "MCP"]], + ["protocol — A2A", ["--name", "my_agent", ...template, "--protocol", "A2A"]], + ["protocol — AGUI", ["--name", "my_agent", ...template, "--protocol", "AGUI"]], + [ + "request-header-allowlist", + [ + "--name", + "my_agent", + ...template, + "--request-header-allowlist", + "X-Custom-Header", + "Authorization", + ], + ], + [ + "lifecycle-configuration", + [ + "--name", + "my_agent", + ...template, + "--lifecycle-configuration", + '{"idleRuntimeSessionTimeout":300,"maxLifetime":3600}', + ], + ], + [ + "environment-variables", + [ + "--name", + "my_agent", + ...template, + "--environment-variables", + '{"LOG_LEVEL":"debug","APP_ENV":"staging"}', + ], + ], + [ + "filesystem-configurations — sessionStorage", + [ + "--name", + "my_agent", + ...template, + "--filesystem-configurations", + '[{"sessionStorage":{"mountPath":"/mnt/data"}}]', + ], + ], + [ + "filesystem-configurations — efsAccessPoint", + [ + "--name", + "my_agent", + ...template, + "--filesystem-configurations", + '[{"efsAccessPoint":{"accessPointArn":"arn:aws:elasticfilesystem:us-east-1:123456789012:access-point/fsap-0123456789abcdef0","mountPath":"/mnt/efs"}}]', + ], + ], + [ + "filesystem-configurations — s3FilesAccessPoint", + [ + "--name", + "my_agent", + ...template, + "--filesystem-configurations", + '[{"s3FilesAccessPoint":{"accessPointArn":"arn:aws:s3files:us-east-1:123456789012:file-system/fs-0123456789abcdef01/access-point/fsap-0123456789abcdef1","mountPath":"/mnt/s3"}}]', + ], + ], + ["tags", ["--name", "my_agent", ...template, "--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", + ...template, + "--additional-policies", + "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"], + ], + [ + "network-config with vpcId", + [ + "--name", + "my_agent", + ...byo, + "--build", + "Container", + "--dockerfile", + "Dockerfile", + "--network-mode", + "VPC", + "--network-config", + '{"subnets":["subnet-0123456789abcdef0"],"securityGroups":["sg-0123456789abcdef0"],"vpcId":"vpc-0123456789abcdef0"}', + ], + ], + ])("%s — accepts flags", async (_label, flags) => { + await inProject(); + await expect(run(["add", "runtime", ...flags])).rejects.toBeInstanceOf(NotImplementedError); + }); + + test.each<[string, string[]]>([ + ["missing --name", ["--template", "hello-world-python"]], + [ + "--template and --code-location are mutually exclusive", + ["--name", "my_agent", "--template", "hello-world-python", "--code-location", "app/agent"], + ], + [ + "--custom-docker-build-args requires --dockerfile or --build-context-path", + [ + "--name", + "my_agent", + ...byo, + "--build", + "Container", + "--custom-docker-build-args", + '{"KEY":"value"}', + ], + ], + [ + "invalid JSON in --network-config", + ["--name", "my_agent", ...template, "--network-config", "{bad}"], + ], + [ + "--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/add/runtime/index.ts b/src/handlers/project/add/runtime/index.ts new file mode 100644 index 000000000..ea900d648 --- /dev/null +++ b/src/handlers/project/add/runtime/index.ts @@ -0,0 +1,238 @@ +import z from "zod"; +import { createHandler, flag, ProjectKey } from "../../../../router"; +import type { AddProjectResourceConfig } from "../types"; +import { parseJsonFlag, parseTags } from "../../../utils"; +import { InputValidationError } from "../../../../errors"; +import { type EnvVar, BuildTypeSchema } from "../../../../projectSchemas/runtime"; +import { RuntimeAuthorizerTypeSchema } from "../../../../projectSchemas/auth"; +import { + NetworkModeSchema, + ProtocolModeSchema, + RuntimeVersionSchema, +} from "../../../../projectSchemas/constants"; +import { SourceResolver } from "../../../../io"; +import { RUNTIME_TEMPLATES } from "../../types"; +import { + runtimeMemoryConfigSchema, + runtimeModelProviderSchema, + RuntimeResourceConfigSchema, +} from "./types"; + +export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => + createHandler({ + name: "runtime", + 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", "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", BuildTypeSchema.optional()), + flag("entrypoint", "entrypoint file, e.g. main.py:handler (BYO only)", z.string().optional()), + flag("protocol", "server protocol: HTTP, MCP, A2A, AGUI", ProtocolModeSchema.optional()), + flag( + "api-key", + "API key source for non-bedrock model providers: '-' for stdin, 'file://path' for file", + z.string().optional(), + { sensitive: true }, + ), + flag( + "model-provider", + "model provider (template only)", + runtimeModelProviderSchema.optional(), + ), + flag( + "runtime-version", + "language runtime, e.g. PYTHON_3_13, NODE_22 (BYO CodeZip only)", + RuntimeVersionSchema.optional(), + ), + flag( + "dockerfile", + "dockerfile path 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( + "additional-policies", + "additional IAM policy ARNs or policy document paths for the execution role", + z.array(z.string()).optional(), + ), + flag( + "network-mode", + "network mode for the runtime environment (PUBLIC or VPC)", + NetworkModeSchema.optional(), + ), + flag("network-config", "VPC network configuration (JSON)", z.string().optional()), + flag( + "authorizer-type", + "inbound authorizer type (AWS_IAM or CUSTOM_JWT)", + RuntimeAuthorizerTypeSchema.optional(), + ), + flag( + "authorizer-configuration", + "inbound authorizer configuration (JSON)", + z.string().optional(), + ), + flag( + "request-header-allowlist", + "request headers to pass through to the runtime", + z.array(z.string()).optional(), + ), + flag("lifecycle-configuration", "lifecycle configuration (JSON)", z.string().optional()), + flag( + "environment-variables", + "environment variables (JSON object of key/value strings)", + z.string().optional(), + ), + flag( + "filesystem-configurations", + "filesystem mount configurations (JSON)", + z.string().optional(), + ), + flag( + "memory", + "memory configuration (JSON with mode: disabled | create | existing) (template only)", + z.string().optional(), + ), + flag("tags", "tags as key=value (repeatable) or JSON object", z.array(z.string()).optional()), + ], + handle: async (ctx, flags) => { + if (!flags.name) + throw new InputValidationError("required option '--name ' not specified"); + + if (flags.template && flags["code-location"]) + throw new InputValidationError("--template and --code-location are mutually exclusive"); + + 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], + ); + 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 inputEnvironmentVariables = parseJsonFlag>( + "environment-variables", + flags["environment-variables"], + ); + const memoryConfiguration = parseMemoryConfig(flags["memory"]); + + const entrypoint = flags.entrypoint ?? "main.py"; + + 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", + ); + + const infraConfig = { + name: flags.name, + description: flags.description, + executionRoleArn: flags["role-arn"], + additionalPolicies: flags["additional-policies"], + envVars: toEnvironmentVariables(inputEnvironmentVariables), + networkMode: flags["network-mode"], + networkConfig: parseJsonFlag("network-config", flags["network-config"]), + authorizerType: flags["authorizer-type"], + authorizerConfiguration: parseJsonFlag( + "authorizer-configuration", + flags["authorizer-configuration"], + ), + protocol: flags["protocol"], + requestHeaderAllowlist: flags["request-header-allowlist"], + lifecycleConfiguration: parseJsonFlag( + "lifecycle-configuration", + flags["lifecycle-configuration"], + ), + filesystemConfigurations: parseJsonFlag( + "filesystem-configurations", + flags["filesystem-configurations"], + ), + tags: parseTags(flags["tags"]), + }; + + const runtimeInput = isTemplate + ? { + source: "template" as const, + template, + memory: memoryConfiguration, + modelProvider: { apiKey, provider: flags["model-provider"] }, + ...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 result = RuntimeResourceConfigSchema.safeParse(runtimeInput); + if (!result.success) + throw new InputValidationError(z.prettifyError(result.error), { cause: result.error }); + + const project = ctx.require(ProjectKey); + for await (const event of config.projectManager.addResource(project, { + resourceType: "runtime", + resourceConfig: result.data, + })) { + config.io.stderr.write(`${event.message}\n`); + } + + config.io.stderr.write(`added runtime '${flags.name}' to '${project.name}'\n`); + }, + }); + +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; +} + +function toEnvironmentVariables(envVars: Record | undefined): EnvVar[] { + return envVars ? Object.entries(envVars).map(([name, value]) => ({ name, value })) : []; +} diff --git a/src/handlers/project/add/runtime/types.ts b/src/handlers/project/add/runtime/types.ts new file mode 100644 index 000000000..f47fa20ae --- /dev/null +++ b/src/handlers/project/add/runtime/types.ts @@ -0,0 +1,67 @@ +import { MemoryStrategyType } from "@aws-sdk/client-bedrock-agentcore-control"; +import z from "zod"; +import { ProjectRuntimeSchema } from "../../../../projectSchemas/runtime"; +import { RUNTIME_TEMPLATES } from "../../types"; + +export const runtimeModelProviderSchema = z.enum(["bedrock", "anthropic", "openai", "gemini"]); +export type RuntimeModelProvider = z.infer; + +export const runtimeModelProviderConfigSchema = z.object({ + provider: runtimeModelProviderSchema.optional(), + apiKey: z.string().min(1).optional(), +}); +export type RuntimeModelProviderConfig = z.infer; + +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; + +const RuntimeInfraConfigSchema = z.object({ + name: ProjectRuntimeSchema.shape.name, + description: ProjectRuntimeSchema.shape.description, + executionRoleArn: ProjectRuntimeSchema.shape.executionRoleArn, + additionalPolicies: ProjectRuntimeSchema.shape.additionalPolicies, + envVars: ProjectRuntimeSchema.shape.envVars, + networkMode: ProjectRuntimeSchema.shape.networkMode, + networkConfig: ProjectRuntimeSchema.shape.networkConfig, + authorizerType: ProjectRuntimeSchema.shape.authorizerType, + authorizerConfiguration: ProjectRuntimeSchema.shape.authorizerConfiguration, + protocol: ProjectRuntimeSchema.shape.protocol, + requestHeaderAllowlist: ProjectRuntimeSchema.shape.requestHeaderAllowlist, + lifecycleConfiguration: ProjectRuntimeSchema.shape.lifecycleConfiguration, + filesystemConfigurations: ProjectRuntimeSchema.shape.filesystemConfigurations, + tags: ProjectRuntimeSchema.shape.tags, +}); + +const RuntimeByoConfigSchema = RuntimeInfraConfigSchema.extend({ + source: z.literal("byo"), + codeLocation: z.string().min(1), + build: ProjectRuntimeSchema.shape.build.optional(), + entrypoint: ProjectRuntimeSchema.shape.entrypoint.optional(), + runtimeVersion: ProjectRuntimeSchema.shape.runtimeVersion, + dockerfile: ProjectRuntimeSchema.shape.dockerfile, + buildContextPath: ProjectRuntimeSchema.shape.buildContextPath, + customDockerBuildArgs: ProjectRuntimeSchema.shape.customDockerBuildArgs, +}); + +const RuntimeTemplateConfigSchema = RuntimeInfraConfigSchema.extend({ + source: z.literal("template"), + template: z.enum(RUNTIME_TEMPLATES), + memory: runtimeMemoryConfigSchema.optional(), + modelProvider: runtimeModelProviderConfigSchema.optional(), +}); + +export const RuntimeResourceConfigSchema = z.discriminatedUnion("source", [ + RuntimeByoConfigSchema, + RuntimeTemplateConfigSchema, +]); +export type RuntimeResourceConfig = z.infer; diff --git a/src/handlers/project/types.ts b/src/handlers/project/types.ts index 560bf0080..b51ab56ef 100644 --- a/src/handlers/project/types.ts +++ b/src/handlers/project/types.ts @@ -1,16 +1,23 @@ import { HarnessSpecSchema } from "../../projectSchemas/harness"; import type { ConfigBundleSchema } from "../../projectSchemas/config-bundle"; import type { ProjectSpecSchema } from "../../projectSchemas/project"; -import type z from "zod"; -import type { ProjectRuntimeSchema } from "../../projectSchemas/runtime"; +import z from "zod"; +import type { RuntimeResourceConfig } from "./add/runtime/types"; import type { OnlineEvalConfigSchema } from "../../projectSchemas/online-eval-config"; -/** 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} describing runtimes only */ +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 = { @@ -50,7 +57,7 @@ export type AddResourceInput = } | { resourceType: "runtime"; - resourceConfig: z.input; + resourceConfig: RuntimeResourceConfig; } | { resourceType: "config-bundle"; diff --git a/src/projectSchemas/runtime.ts b/src/projectSchemas/runtime.ts index 61dc95886..aba8d291b 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 {