Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/handlers/project/add/index.ts
Original file line numberDiff line numberDiff line change
@@ -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;
}
370 changes: 370 additions & 0 deletions src/handlers/project/add/runtime/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
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 {
AuthorizerConfiguration,
FilesystemConfiguration,
LifecycleConfiguration,
NetworkConfiguration,
ProtocolConfiguration,
RequestHeaderConfiguration,
} from "@aws-sdk/client-bedrock-agentcore-control";
import {
type EnvVar,
type FilesystemConfiguration as ProjectFilesystemConfiguration,
type NetworkConfig,
BuildTypeSchema,
} from "../../../../projectSchemas/runtime";
import type { AuthorizerConfig, RuntimeAuthorizerType } from "../../../../projectSchemas/auth";
import {
type NetworkMode,
ProtocolModeSchema,
RuntimeVersionSchema,
} from "../../../../projectSchemas/constants";
import {
runtimeModelProviderSchema,
RUNTIME_TEMPLATES,
runtimeMemoryConfigSchema,
} from "../../types";
import { SourceResolver } from "../../../../io";

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 ex. 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(),
),
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-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)",
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(
"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) => {
if (!flags.name)
throw new InputValidationError("required option '--name <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 inputNetwork = parseJsonFlag<NetworkConfiguration>(
"network-configuration",
flags["network-configuration"],
);
const inputAuthConfig = parseJsonFlag<AuthorizerConfiguration>(
"authorizer-configuration",
flags["authorizer-configuration"],
);
const inputProtocol = parseJsonFlag<ProtocolConfiguration>(
"protocol-configuration",
flags["protocol-configuration"],
);
const inputRequestHeaders = parseJsonFlag<RequestHeaderConfiguration>(
"request-header-configuration",
flags["request-header-configuration"],
);
const inputLifecycle = parseJsonFlag<LifecycleConfiguration>(
"lifecycle-configuration",
flags["lifecycle-configuration"],
);
const inputFilesystems = parseJsonFlag<FilesystemConfiguration[]>(
"filesystem-configurations",
flags["filesystem-configurations"],
);
const inputEnvironmentVariables = parseJsonFlag<Record<string, string>>(
"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",
);

if (flags["vpc-id"] && !network?.networkConfig)
throw new InputValidationError(
"--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);

const infraConfig = {
name: flags.name,
description: flags.description,
executionRoleArn: flags["role-arn"],
additionalPolicies: flags["additional-policies"],
envVars: toEnvironmentVariables(inputEnvironmentVariables),
networkMode: network?.networkMode,
networkConfig: network?.networkConfig
? { ...network.networkConfig, ...(flags["vpc-id"] ? { vpcId: flags["vpc-id"] } : {}) }
: undefined,
authorizerType: auth?.authorizerType,
authorizerConfiguration: auth?.authorizerConfiguration,
protocol: flags["protocol"] ?? inputProtocol?.serverProtocol,
requestHeaderAllowlist,
lifecycleConfiguration: inputLifecycle,
filesystemConfigurations,
tags: parseJsonFlag<Record<string, string>>("tags", flags["tags"]),
};

const runtimeConfig = 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<Record<string, string>>(
"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",
resourceConfig: runtimeConfig,
})) {
config.io.stderr.write(`${event.message}\n`);
}

config.io.stderr.write(`added runtime '${flags.name}' to '${project.name}'\n`);
},
});

/** Parses and validates the --memory JSON flag against the runtime memory config schema. */
function parseMemoryConfig(
raw: string | undefined,
): z.infer<typeof runtimeMemoryConfigSchema> | undefined {
if (!raw) return undefined;
const parsed = parseJsonFlag<Record<string, unknown>>("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<string, string> | 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");
}

/** 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;
}
throw new InputValidationError("Unrecognized request header configuration variant");
}

/** 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");
});
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/handlers/project/add/index.ts
Original file line numberDiff line numberDiff line change
@@ -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;
}
370 changes: 370 additions & 0 deletions src/handlers/project/add/runtime/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
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 {
AuthorizerConfiguration,
FilesystemConfiguration,
LifecycleConfiguration,
NetworkConfiguration,
ProtocolConfiguration,
RequestHeaderConfiguration,
} from "@aws-sdk/client-bedrock-agentcore-control";
import {
type EnvVar,
type FilesystemConfiguration as ProjectFilesystemConfiguration,
type NetworkConfig,
BuildTypeSchema,
} from "../../../../projectSchemas/runtime";
import type { AuthorizerConfig, RuntimeAuthorizerType } from "../../../../projectSchemas/auth";
import {
type NetworkMode,
ProtocolModeSchema,
RuntimeVersionSchema,
} from "../../../../projectSchemas/constants";
import {
runtimeModelProviderSchema,
RUNTIME_TEMPLATES,
runtimeMemoryConfigSchema,
} from "../../types";
import { SourceResolver } from "../../../../io";

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 ex. 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(),
),
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-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)",
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(
"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) => {
if (!flags.name)
throw new InputValidationError("required option '--name <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 inputNetwork = parseJsonFlag<NetworkConfiguration>(
"network-configuration",
flags["network-configuration"],
);
const inputAuthConfig = parseJsonFlag<AuthorizerConfiguration>(
"authorizer-configuration",
flags["authorizer-configuration"],
);
const inputProtocol = parseJsonFlag<ProtocolConfiguration>(
"protocol-configuration",
flags["protocol-configuration"],
);
const inputRequestHeaders = parseJsonFlag<RequestHeaderConfiguration>(
"request-header-configuration",
flags["request-header-configuration"],
);
const inputLifecycle = parseJsonFlag<LifecycleConfiguration>(
"lifecycle-configuration",
flags["lifecycle-configuration"],
);
const inputFilesystems = parseJsonFlag<FilesystemConfiguration[]>(
"filesystem-configurations",
flags["filesystem-configurations"],
);
const inputEnvironmentVariables = parseJsonFlag<Record<string, string>>(
"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",
);

if (flags["vpc-id"] && !network?.networkConfig)
throw new InputValidationError(
"--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);

const infraConfig = {
name: flags.name,
description: flags.description,
executionRoleArn: flags["role-arn"],
additionalPolicies: flags["additional-policies"],
envVars: toEnvironmentVariables(inputEnvironmentVariables),
networkMode: network?.networkMode,
networkConfig: network?.networkConfig
? { ...network.networkConfig, ...(flags["vpc-id"] ? { vpcId: flags["vpc-id"] } : {}) }
: undefined,
authorizerType: auth?.authorizerType,
authorizerConfiguration: auth?.authorizerConfiguration,
protocol: flags["protocol"] ?? inputProtocol?.serverProtocol,
requestHeaderAllowlist,
lifecycleConfiguration: inputLifecycle,
filesystemConfigurations,
tags: parseJsonFlag<Record<string, string>>("tags", flags["tags"]),
};

const runtimeConfig = 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<Record<string, string>>(
"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",
resourceConfig: runtimeConfig,
})) {
config.io.stderr.write(`${event.message}\n`);
}

config.io.stderr.write(`added runtime '${flags.name}' to '${project.name}'\n`);
},
});

/** Parses and validates the --memory JSON flag against the runtime memory config schema. */
function parseMemoryConfig(
raw: string | undefined,
): z.infer<typeof runtimeMemoryConfigSchema> | undefined {
if (!raw) return undefined;
const parsed = parseJsonFlag<Record<string, unknown>>("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<string, string> | 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");
}

/** 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;
}
throw new InputValidationError("Unrecognized request header configuration variant");
}

/** 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");
});
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/handlers/project/add/index.ts
Original file line numberDiff line numberDiff line change
@@ -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;
}
370 changes: 370 additions & 0 deletions src/handlers/project/add/runtime/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
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 {
AuthorizerConfiguration,
FilesystemConfiguration,
LifecycleConfiguration,
NetworkConfiguration,
ProtocolConfiguration,
RequestHeaderConfiguration,
} from "@aws-sdk/client-bedrock-agentcore-control";
import {
type EnvVar,
type FilesystemConfiguration as ProjectFilesystemConfiguration,
type NetworkConfig,
BuildTypeSchema,
} from "../../../../projectSchemas/runtime";
import type { AuthorizerConfig, RuntimeAuthorizerType } from "../../../../projectSchemas/auth";
import {
type NetworkMode,
ProtocolModeSchema,
RuntimeVersionSchema,
} from "../../../../projectSchemas/constants";
import {
runtimeModelProviderSchema,
RUNTIME_TEMPLATES,
runtimeMemoryConfigSchema,
} from "../../types";
import { SourceResolver } from "../../../../io";

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 ex. 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(),
),
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-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)",
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(
"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) => {
if (!flags.name)
throw new InputValidationError("required option '--name <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 inputNetwork = parseJsonFlag<NetworkConfiguration>(
"network-configuration",
flags["network-configuration"],
);
const inputAuthConfig = parseJsonFlag<AuthorizerConfiguration>(
"authorizer-configuration",
flags["authorizer-configuration"],
);
const inputProtocol = parseJsonFlag<ProtocolConfiguration>(
"protocol-configuration",
flags["protocol-configuration"],
);
const inputRequestHeaders = parseJsonFlag<RequestHeaderConfiguration>(
"request-header-configuration",
flags["request-header-configuration"],
);
const inputLifecycle = parseJsonFlag<LifecycleConfiguration>(
"lifecycle-configuration",
flags["lifecycle-configuration"],
);
const inputFilesystems = parseJsonFlag<FilesystemConfiguration[]>(
"filesystem-configurations",
flags["filesystem-configurations"],
);
const inputEnvironmentVariables = parseJsonFlag<Record<string, string>>(
"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",
);

if (flags["vpc-id"] && !network?.networkConfig)
throw new InputValidationError(
"--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);

const infraConfig = {
name: flags.name,
description: flags.description,
executionRoleArn: flags["role-arn"],
additionalPolicies: flags["additional-policies"],
envVars: toEnvironmentVariables(inputEnvironmentVariables),
networkMode: network?.networkMode,
networkConfig: network?.networkConfig
? { ...network.networkConfig, ...(flags["vpc-id"] ? { vpcId: flags["vpc-id"] } : {}) }
: undefined,
authorizerType: auth?.authorizerType,
authorizerConfiguration: auth?.authorizerConfiguration,
protocol: flags["protocol"] ?? inputProtocol?.serverProtocol,
requestHeaderAllowlist,
lifecycleConfiguration: inputLifecycle,
filesystemConfigurations,
tags: parseJsonFlag<Record<string, string>>("tags", flags["tags"]),
};

const runtimeConfig = 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<Record<string, string>>(
"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",
resourceConfig: runtimeConfig,
})) {
config.io.stderr.write(`${event.message}\n`);
}

config.io.stderr.write(`added runtime '${flags.name}' to '${project.name}'\n`);
},
});

/** Parses and validates the --memory JSON flag against the runtime memory config schema. */
function parseMemoryConfig(
raw: string | undefined,
): z.infer<typeof runtimeMemoryConfigSchema> | undefined {
if (!raw) return undefined;
const parsed = parseJsonFlag<Record<string, unknown>>("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<string, string> | 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");
}

/** 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;
}
throw new InputValidationError("Unrecognized request header configuration variant");
}

/** 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");
});
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/handlers/project/add/index.ts
Original file line numberDiff line numberDiff line change
@@ -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;
}
370 changes: 370 additions & 0 deletions src/handlers/project/add/runtime/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
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 {
AuthorizerConfiguration,
FilesystemConfiguration,
LifecycleConfiguration,
NetworkConfiguration,
ProtocolConfiguration,
RequestHeaderConfiguration,
} from "@aws-sdk/client-bedrock-agentcore-control";
import {
type EnvVar,
type FilesystemConfiguration as ProjectFilesystemConfiguration,
type NetworkConfig,
BuildTypeSchema,
} from "../../../../projectSchemas/runtime";
import type { AuthorizerConfig, RuntimeAuthorizerType } from "../../../../projectSchemas/auth";
import {
type NetworkMode,
ProtocolModeSchema,
RuntimeVersionSchema,
} from "../../../../projectSchemas/constants";
import {
runtimeModelProviderSchema,
RUNTIME_TEMPLATES,
runtimeMemoryConfigSchema,
} from "../../types";
import { SourceResolver } from "../../../../io";

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 ex. 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(),
),
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-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)",
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(
"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) => {
if (!flags.name)
throw new InputValidationError("required option '--name <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 inputNetwork = parseJsonFlag<NetworkConfiguration>(
"network-configuration",
flags["network-configuration"],
);
const inputAuthConfig = parseJsonFlag<AuthorizerConfiguration>(
"authorizer-configuration",
flags["authorizer-configuration"],
);
const inputProtocol = parseJsonFlag<ProtocolConfiguration>(
"protocol-configuration",
flags["protocol-configuration"],
);
const inputRequestHeaders = parseJsonFlag<RequestHeaderConfiguration>(
"request-header-configuration",
flags["request-header-configuration"],
);
const inputLifecycle = parseJsonFlag<LifecycleConfiguration>(
"lifecycle-configuration",
flags["lifecycle-configuration"],
);
const inputFilesystems = parseJsonFlag<FilesystemConfiguration[]>(
"filesystem-configurations",
flags["filesystem-configurations"],
);
const inputEnvironmentVariables = parseJsonFlag<Record<string, string>>(
"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",
);

if (flags["vpc-id"] && !network?.networkConfig)
throw new InputValidationError(
"--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);

const infraConfig = {
name: flags.name,
description: flags.description,
executionRoleArn: flags["role-arn"],
additionalPolicies: flags["additional-policies"],
envVars: toEnvironmentVariables(inputEnvironmentVariables),
networkMode: network?.networkMode,
networkConfig: network?.networkConfig
? { ...network.networkConfig, ...(flags["vpc-id"] ? { vpcId: flags["vpc-id"] } : {}) }
: undefined,
authorizerType: auth?.authorizerType,
authorizerConfiguration: auth?.authorizerConfiguration,
protocol: flags["protocol"] ?? inputProtocol?.serverProtocol,
requestHeaderAllowlist,
lifecycleConfiguration: inputLifecycle,
filesystemConfigurations,
tags: parseJsonFlag<Record<string, string>>("tags", flags["tags"]),
};

const runtimeConfig = 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<Record<string, string>>(
"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",
resourceConfig: runtimeConfig,
})) {
config.io.stderr.write(`${event.message}\n`);
}

config.io.stderr.write(`added runtime '${flags.name}' to '${project.name}'\n`);
},
});

/** Parses and validates the --memory JSON flag against the runtime memory config schema. */
function parseMemoryConfig(
raw: string | undefined,
): z.infer<typeof runtimeMemoryConfigSchema> | undefined {
if (!raw) return undefined;
const parsed = parseJsonFlag<Record<string, unknown>>("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<string, string> | 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");
}

/** 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;
}
throw new InputValidationError("Unrecognized request header configuration variant");
}

/** 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");
});
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/handlers/project/add/index.ts
Original file line numberDiff line numberDiff line change
@@ -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;
}
370 changes: 370 additions & 0 deletions src/handlers/project/add/runtime/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
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 {
AuthorizerConfiguration,
FilesystemConfiguration,
LifecycleConfiguration,
NetworkConfiguration,
ProtocolConfiguration,
RequestHeaderConfiguration,
} from "@aws-sdk/client-bedrock-agentcore-control";
import {
type EnvVar,
type FilesystemConfiguration as ProjectFilesystemConfiguration,
type NetworkConfig,
BuildTypeSchema,
} from "../../../../projectSchemas/runtime";
import type { AuthorizerConfig, RuntimeAuthorizerType } from "../../../../projectSchemas/auth";
import {
type NetworkMode,
ProtocolModeSchema,
RuntimeVersionSchema,
} from "../../../../projectSchemas/constants";
import {
runtimeModelProviderSchema,
RUNTIME_TEMPLATES,
runtimeMemoryConfigSchema,
} from "../../types";
import { SourceResolver } from "../../../../io";

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 ex. 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(),
),
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-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)",
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(
"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) => {
if (!flags.name)
throw new InputValidationError("required option '--name <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 inputNetwork = parseJsonFlag<NetworkConfiguration>(
"network-configuration",
flags["network-configuration"],
);
const inputAuthConfig = parseJsonFlag<AuthorizerConfiguration>(
"authorizer-configuration",
flags["authorizer-configuration"],
);
const inputProtocol = parseJsonFlag<ProtocolConfiguration>(
"protocol-configuration",
flags["protocol-configuration"],
);
const inputRequestHeaders = parseJsonFlag<RequestHeaderConfiguration>(
"request-header-configuration",
flags["request-header-configuration"],
);
const inputLifecycle = parseJsonFlag<LifecycleConfiguration>(
"lifecycle-configuration",
flags["lifecycle-configuration"],
);
const inputFilesystems = parseJsonFlag<FilesystemConfiguration[]>(
"filesystem-configurations",
flags["filesystem-configurations"],
);
const inputEnvironmentVariables = parseJsonFlag<Record<string, string>>(
"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",
);

if (flags["vpc-id"] && !network?.networkConfig)
throw new InputValidationError(
"--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);

const infraConfig = {
name: flags.name,
description: flags.description,
executionRoleArn: flags["role-arn"],
additionalPolicies: flags["additional-policies"],
envVars: toEnvironmentVariables(inputEnvironmentVariables),
networkMode: network?.networkMode,
networkConfig: network?.networkConfig
? { ...network.networkConfig, ...(flags["vpc-id"] ? { vpcId: flags["vpc-id"] } : {}) }
: undefined,
authorizerType: auth?.authorizerType,
authorizerConfiguration: auth?.authorizerConfiguration,
protocol: flags["protocol"] ?? inputProtocol?.serverProtocol,
requestHeaderAllowlist,
lifecycleConfiguration: inputLifecycle,
filesystemConfigurations,
tags: parseJsonFlag<Record<string, string>>("tags", flags["tags"]),
};

const runtimeConfig = 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<Record<string, string>>(
"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",
resourceConfig: runtimeConfig,
})) {
config.io.stderr.write(`${event.message}\n`);
}

config.io.stderr.write(`added runtime '${flags.name}' to '${project.name}'\n`);
},
});

/** Parses and validates the --memory JSON flag against the runtime memory config schema. */
function parseMemoryConfig(
raw: string | undefined,
): z.infer<typeof runtimeMemoryConfigSchema> | undefined {
if (!raw) return undefined;
const parsed = parseJsonFlag<Record<string, unknown>>("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<string, string> | 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");
}

/** 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;
}
throw new InputValidationError("Unrecognized request header configuration variant");
}

/** 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");
});
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/handlers/project/add/index.ts
Original file line numberDiff line numberDiff line change
@@ -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;
}
370 changes: 370 additions & 0 deletions src/handlers/project/add/runtime/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
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 {
AuthorizerConfiguration,
FilesystemConfiguration,
LifecycleConfiguration,
NetworkConfiguration,
ProtocolConfiguration,
RequestHeaderConfiguration,
} from "@aws-sdk/client-bedrock-agentcore-control";
import {
type EnvVar,
type FilesystemConfiguration as ProjectFilesystemConfiguration,
type NetworkConfig,
BuildTypeSchema,
} from "../../../../projectSchemas/runtime";
import type { AuthorizerConfig, RuntimeAuthorizerType } from "../../../../projectSchemas/auth";
import {
type NetworkMode,
ProtocolModeSchema,
RuntimeVersionSchema,
} from "../../../../projectSchemas/constants";
import {
runtimeModelProviderSchema,
RUNTIME_TEMPLATES,
runtimeMemoryConfigSchema,
} from "../../types";
import { SourceResolver } from "../../../../io";

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 ex. 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(),
),
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-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)",
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(
"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) => {
if (!flags.name)
throw new InputValidationError("required option '--name <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 inputNetwork = parseJsonFlag<NetworkConfiguration>(
"network-configuration",
flags["network-configuration"],
);
const inputAuthConfig = parseJsonFlag<AuthorizerConfiguration>(
"authorizer-configuration",
flags["authorizer-configuration"],
);
const inputProtocol = parseJsonFlag<ProtocolConfiguration>(
"protocol-configuration",
flags["protocol-configuration"],
);
const inputRequestHeaders = parseJsonFlag<RequestHeaderConfiguration>(
"request-header-configuration",
flags["request-header-configuration"],
);
const inputLifecycle = parseJsonFlag<LifecycleConfiguration>(
"lifecycle-configuration",
flags["lifecycle-configuration"],
);
const inputFilesystems = parseJsonFlag<FilesystemConfiguration[]>(
"filesystem-configurations",
flags["filesystem-configurations"],
);
const inputEnvironmentVariables = parseJsonFlag<Record<string, string>>(
"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",
);

if (flags["vpc-id"] && !network?.networkConfig)
throw new InputValidationError(
"--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);

const infraConfig = {
name: flags.name,
description: flags.description,
executionRoleArn: flags["role-arn"],
additionalPolicies: flags["additional-policies"],
envVars: toEnvironmentVariables(inputEnvironmentVariables),
networkMode: network?.networkMode,
networkConfig: network?.networkConfig
? { ...network.networkConfig, ...(flags["vpc-id"] ? { vpcId: flags["vpc-id"] } : {}) }
: undefined,
authorizerType: auth?.authorizerType,
authorizerConfiguration: auth?.authorizerConfiguration,
protocol: flags["protocol"] ?? inputProtocol?.serverProtocol,
requestHeaderAllowlist,
lifecycleConfiguration: inputLifecycle,
filesystemConfigurations,
tags: parseJsonFlag<Record<string, string>>("tags", flags["tags"]),
};

const runtimeConfig = 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<Record<string, string>>(
"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",
resourceConfig: runtimeConfig,
})) {
config.io.stderr.write(`${event.message}\n`);
}

config.io.stderr.write(`added runtime '${flags.name}' to '${project.name}'\n`);
},
});

/** Parses and validates the --memory JSON flag against the runtime memory config schema. */
function parseMemoryConfig(
raw: string | undefined,
): z.infer<typeof runtimeMemoryConfigSchema> | undefined {
if (!raw) return undefined;
const parsed = parseJsonFlag<Record<string, unknown>>("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<string, string> | 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");
}

/** 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;
}
throw new InputValidationError("Unrecognized request header configuration variant");
}

/** 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");
});
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/handlers/project/add/index.ts
Original file line numberDiff line numberDiff line change
@@ -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;
}
370 changes: 370 additions & 0 deletions src/handlers/project/add/runtime/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
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 {
AuthorizerConfiguration,
FilesystemConfiguration,
LifecycleConfiguration,
NetworkConfiguration,
ProtocolConfiguration,
RequestHeaderConfiguration,
} from "@aws-sdk/client-bedrock-agentcore-control";
import {
type EnvVar,
type FilesystemConfiguration as ProjectFilesystemConfiguration,
type NetworkConfig,
BuildTypeSchema,
} from "../../../../projectSchemas/runtime";
import type { AuthorizerConfig, RuntimeAuthorizerType } from "../../../../projectSchemas/auth";
import {
type NetworkMode,
ProtocolModeSchema,
RuntimeVersionSchema,
} from "../../../../projectSchemas/constants";
import {
runtimeModelProviderSchema,
RUNTIME_TEMPLATES,
runtimeMemoryConfigSchema,
} from "../../types";
import { SourceResolver } from "../../../../io";

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 ex. 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(),
),
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-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)",
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(
"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) => {
if (!flags.name)
throw new InputValidationError("required option '--name <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 inputNetwork = parseJsonFlag<NetworkConfiguration>(
"network-configuration",
flags["network-configuration"],
);
const inputAuthConfig = parseJsonFlag<AuthorizerConfiguration>(
"authorizer-configuration",
flags["authorizer-configuration"],
);
const inputProtocol = parseJsonFlag<ProtocolConfiguration>(
"protocol-configuration",
flags["protocol-configuration"],
);
const inputRequestHeaders = parseJsonFlag<RequestHeaderConfiguration>(
"request-header-configuration",
flags["request-header-configuration"],
);
const inputLifecycle = parseJsonFlag<LifecycleConfiguration>(
"lifecycle-configuration",
flags["lifecycle-configuration"],
);
const inputFilesystems = parseJsonFlag<FilesystemConfiguration[]>(
"filesystem-configurations",
flags["filesystem-configurations"],
);
const inputEnvironmentVariables = parseJsonFlag<Record<string, string>>(
"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",
);

if (flags["vpc-id"] && !network?.networkConfig)
throw new InputValidationError(
"--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);

const infraConfig = {
name: flags.name,
description: flags.description,
executionRoleArn: flags["role-arn"],
additionalPolicies: flags["additional-policies"],
envVars: toEnvironmentVariables(inputEnvironmentVariables),
networkMode: network?.networkMode,
networkConfig: network?.networkConfig
? { ...network.networkConfig, ...(flags["vpc-id"] ? { vpcId: flags["vpc-id"] } : {}) }
: undefined,
authorizerType: auth?.authorizerType,
authorizerConfiguration: auth?.authorizerConfiguration,
protocol: flags["protocol"] ?? inputProtocol?.serverProtocol,
requestHeaderAllowlist,
lifecycleConfiguration: inputLifecycle,
filesystemConfigurations,
tags: parseJsonFlag<Record<string, string>>("tags", flags["tags"]),
};

const runtimeConfig = 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<Record<string, string>>(
"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",
resourceConfig: runtimeConfig,
})) {
config.io.stderr.write(`${event.message}\n`);
}

config.io.stderr.write(`added runtime '${flags.name}' to '${project.name}'\n`);
},
});

/** Parses and validates the --memory JSON flag against the runtime memory config schema. */
function parseMemoryConfig(
raw: string | undefined,
): z.infer<typeof runtimeMemoryConfigSchema> | undefined {
if (!raw) return undefined;
const parsed = parseJsonFlag<Record<string, unknown>>("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<string, string> | 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");
}

/** 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;
}
throw new InputValidationError("Unrecognized request header configuration variant");
}

/** 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");
});
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/handlers/project/add/index.ts
Original file line numberDiff line numberDiff line change
@@ -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;
}
370 changes: 370 additions & 0 deletions src/handlers/project/add/runtime/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
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 {
AuthorizerConfiguration,
FilesystemConfiguration,
LifecycleConfiguration,
NetworkConfiguration,
ProtocolConfiguration,
RequestHeaderConfiguration,
} from "@aws-sdk/client-bedrock-agentcore-control";
import {
type EnvVar,
type FilesystemConfiguration as ProjectFilesystemConfiguration,
type NetworkConfig,
BuildTypeSchema,
} from "../../../../projectSchemas/runtime";
import type { AuthorizerConfig, RuntimeAuthorizerType } from "../../../../projectSchemas/auth";
import {
type NetworkMode,
ProtocolModeSchema,
RuntimeVersionSchema,
} from "../../../../projectSchemas/constants";
import {
runtimeModelProviderSchema,
RUNTIME_TEMPLATES,
runtimeMemoryConfigSchema,
} from "../../types";
import { SourceResolver } from "../../../../io";

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 ex. 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(),
),
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-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)",
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(
"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) => {
if (!flags.name)
throw new InputValidationError("required option '--name <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 inputNetwork = parseJsonFlag<NetworkConfiguration>(
"network-configuration",
flags["network-configuration"],
);
const inputAuthConfig = parseJsonFlag<AuthorizerConfiguration>(
"authorizer-configuration",
flags["authorizer-configuration"],
);
const inputProtocol = parseJsonFlag<ProtocolConfiguration>(
"protocol-configuration",
flags["protocol-configuration"],
);
const inputRequestHeaders = parseJsonFlag<RequestHeaderConfiguration>(
"request-header-configuration",
flags["request-header-configuration"],
);
const inputLifecycle = parseJsonFlag<LifecycleConfiguration>(
"lifecycle-configuration",
flags["lifecycle-configuration"],
);
const inputFilesystems = parseJsonFlag<FilesystemConfiguration[]>(
"filesystem-configurations",
flags["filesystem-configurations"],
);
const inputEnvironmentVariables = parseJsonFlag<Record<string, string>>(
"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",
);

if (flags["vpc-id"] && !network?.networkConfig)
throw new InputValidationError(
"--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);

const infraConfig = {
name: flags.name,
description: flags.description,
executionRoleArn: flags["role-arn"],
additionalPolicies: flags["additional-policies"],
envVars: toEnvironmentVariables(inputEnvironmentVariables),
networkMode: network?.networkMode,
networkConfig: network?.networkConfig
? { ...network.networkConfig, ...(flags["vpc-id"] ? { vpcId: flags["vpc-id"] } : {}) }
: undefined,
authorizerType: auth?.authorizerType,
authorizerConfiguration: auth?.authorizerConfiguration,
protocol: flags["protocol"] ?? inputProtocol?.serverProtocol,
requestHeaderAllowlist,
lifecycleConfiguration: inputLifecycle,
filesystemConfigurations,
tags: parseJsonFlag<Record<string, string>>("tags", flags["tags"]),
};

const runtimeConfig = 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<Record<string, string>>(
"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",
resourceConfig: runtimeConfig,
})) {
config.io.stderr.write(`${event.message}\n`);
}

config.io.stderr.write(`added runtime '${flags.name}' to '${project.name}'\n`);
},
});

/** Parses and validates the --memory JSON flag against the runtime memory config schema. */
function parseMemoryConfig(
raw: string | undefined,
): z.infer<typeof runtimeMemoryConfigSchema> | undefined {
if (!raw) return undefined;
const parsed = parseJsonFlag<Record<string, unknown>>("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<string, string> | 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");
}

/** 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;
}
throw new InputValidationError("Unrecognized request header configuration variant");
}

/** 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");
});
}
Loading
Loading