Skip to content
Merged
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
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,33 @@ yourself.
Note that `agentcore status` reports only the resources `agentcore.json`
declares, not the ones you add in the stack.

## China (aws-cn) regions

`cn-north-1` and `cn-northwest-1` are supported, with three differences:

- **Templates:** Amazon Bedrock, Anthropic, OpenAI, and Gemini are not accessible from China
regions, so templates wired to those providers (and `--type import`, which reads from Amazon
Bedrock) are rejected when a deployment target is in a China region. Bring your own agent
implementation instead: scaffold with `agent-python-minimal` or `mcp-python-fastmcp` and add
your own model connectivity, or use `--template agent-python-strands --model-provider litellm
--model-id <model>` with a [LiteLLM model](https://docs.litellm.ai/docs/providers) reachable
from China — no default model id is applied there, and the `bedrock/` LiteLLM prefix (which
routes to Amazon Bedrock) is rejected. The scaffolded runtime records `modelProvider` and,
for LiteLLM, `modelId` in `agentcore.json` so deploys can re-check this.
- The restrictions are enforced wherever the region is known: at `agentcore create` when the
resolved region (`--region`, environment, or profile) is a China region, at
`agentcore add runtime` once deployment targets exist, and at `agentcore deploy` — deploying
to a China target fails when a runtime was scaffolded with an inaccessible model provider
(recorded as the runtime's `modelProvider` in `agentcore.json`; delete that field if you have
replaced the model wiring in code). Harness projects are not available in China regions.
- **Resource families:** only Runtimes, Gateways, and credentials are available in China
regions. The strands template's default memory is dropped from China scaffolds (the memory
module stays in the code and activates once a memory exists); adding unsupported resources
(memory, evaluators, harnesses, payments, …) or deploying a spec that contains them to a
China target fails with an explicit message.
- **Telemetry** is always disabled when the ambient AWS region or any deployment target is a
China region.

## Documentation

- [Amazon Bedrock AgentCore documentation](https://docs.aws.amazon.com/bedrock-agentcore/): service guides and API references.
Expand Down
4 changes: 3 additions & 1 deletion command.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ create a new AgentCore project
- `--name <name>`: name of the project to create (required)
- `--template <template>`: the template to scaffold the Runtime from; some templates also accept --model-provider/--api-key
- `--model-provider <model-provider>`: model provider for templates that support it: bedrock, anthropic, open\_ai, gemini, or lite\_llm
- `--model-id <model-id>`: model id for the scaffolded Runtime code, overriding the provider's default (required with lite\_llm in China regions)
- `--api-key <api-key>`: API key for non-Bedrock providers: '-' for stdin, 'file://path' for file
- `--skip-install`: skip installing dependencies (npm install, uv sync) (default: false)
- `--skip-git`: skip initializing a git repository (default: false)
Expand Down Expand Up @@ -352,7 +353,8 @@ add a Runtime to the current project
- `--name <name>`: the name of the Runtime (required)
- `--type <type>`: create generates new agent code (the default); import translates a Bedrock Agent version
- `--template <template>`: template for the Runtime code (default: agent-python-minimal); available templates listed below
- `--model-provider <model-provider>`: model provider for supported templates (Bedrock, Anthropic, OpenAI, or Gemini)
- `--model-provider <model-provider>`: model provider for supported templates (Bedrock, Anthropic, OpenAI, Gemini, or LiteLLM)
- `--model-id <model-id>`: model id for the scaffolded Runtime code, overriding the provider's default (required with litellm in China regions)
- `--api-key <api-key>`: API key for non-Bedrock providers on supported templates; '-' for stdin, 'file://path' for file
- `--description <description>`: an optional description of the Runtime
- `--tags <tags...>`: tags as key=value (repeatable) or JSON object
Expand Down
10 changes: 5 additions & 5 deletions src/assets/templates/agent-python-strands/model/load.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

def load_model() -> BedrockModel:
"""Get Bedrock model client using IAM credentials."""
return BedrockModel(model_id="global.anthropic.claude-sonnet-4-5-20250929-v1:0")
return BedrockModel(model_id={{safeJson modelId}})
{{/if}}
{{#if (eq modelProvider "Anthropic")}}
import os
Expand Down Expand Up @@ -41,7 +41,7 @@ def load_model() -> AnthropicModel:
"""Get authenticated Anthropic model client."""
return AnthropicModel(
client_args={"api_key": _get_api_key()},
model_id="claude-sonnet-4-5-20250929",
model_id={{safeJson modelId}},
max_tokens=5000,
)
{{/if}}
Expand Down Expand Up @@ -80,7 +80,7 @@ def load_model() -> OpenAIModel:
"""Get authenticated OpenAI model client."""
return OpenAIModel(
client_args={"api_key": _get_api_key()},
model_id="gpt-4.1",
model_id={{safeJson modelId}},
)
{{/if}}
{{#if (eq modelProvider "Gemini")}}
Expand Down Expand Up @@ -118,7 +118,7 @@ def load_model() -> GeminiModel:
"""Get authenticated Gemini model client."""
return GeminiModel(
client_args={"api_key": _get_api_key()},
model_id="gemini-2.5-flash",
model_id={{safeJson modelId}},
)
{{/if}}
{{#if (eq modelProvider "LiteLLM")}}
Expand Down Expand Up @@ -164,6 +164,6 @@ def load_model() -> LiteLLMModel:
{{/if}}
return LiteLLMModel(
client_args=client_args,
model_id="bedrock/us.anthropic.claude-sonnet-4-5-20250514-v1:0",
model_id={{safeJson modelId}},
)
{{/if}}
86 changes: 86 additions & 0 deletions src/core/partition.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { isChinaContext, isChinaRegion } from "./partition";

describe("isChinaRegion", () => {
test.each(["cn-north-1", "cn-northwest-1"])("returns true for %s", (region) => {
expect(isChinaRegion(region)).toBe(true);
});

test.each(["us-east-1", "eu-west-1", "us-gov-west-1", ""])("returns false for %j", (region) => {
expect(isChinaRegion(region)).toBe(false);
});
});

describe("isChinaContext", () => {
let savedRegion: string | undefined;
let savedDefaultRegion: string | undefined;
let directory: string;

beforeEach(async () => {
savedRegion = process.env.AWS_REGION;
savedDefaultRegion = process.env.AWS_DEFAULT_REGION;
delete process.env.AWS_REGION;
delete process.env.AWS_DEFAULT_REGION;
directory = await mkdtemp(join(tmpdir(), "agentcore-partition-"));
});

afterEach(async () => {
if (savedRegion === undefined) delete process.env.AWS_REGION;
else process.env.AWS_REGION = savedRegion;
if (savedDefaultRegion === undefined) delete process.env.AWS_DEFAULT_REGION;
else process.env.AWS_DEFAULT_REGION = savedDefaultRegion;
await rm(directory, { recursive: true, force: true });
});

async function projectWithTargets(targets: unknown): Promise<string> {
const root = join(directory, "project");
await mkdir(join(root, "agentcore"), { recursive: true });
await writeFile(join(root, "agentcore", "agentcore.json"), "{}");
if (targets !== undefined) {
await writeFile(join(root, "agentcore", "aws-targets.json"), JSON.stringify(targets));
}
return root;
}

test("true when the ambient region env var is a China region", async () => {
process.env.AWS_REGION = "cn-northwest-1";
expect(await isChinaContext({ cwd: directory })).toBe(true);
});

test("true when the caller passes a resolved China region", async () => {
expect(await isChinaContext({ region: "cn-north-1", cwd: directory })).toBe(true);
expect(await isChinaContext({ region: "us-west-2", cwd: directory })).toBe(false);
});

test("true when the enclosing project declares a China deployment target", async () => {
const root = await projectWithTargets([
{ name: "primary", account: "111122223333", region: "us-west-2" },
{ name: "china", account: "111122223333", region: "cn-north-1" },
]);
expect(await isChinaContext({ cwd: root })).toBe(true);
// The walk finds the project from a nested path as well.
expect(await isChinaContext({ cwd: join(root, "app", "nested") })).toBe(true);
});

test("false for a project with only commercial targets", async () => {
const root = await projectWithTargets([
{ name: "primary", account: "111122223333", region: "us-west-2" },
]);
expect(await isChinaContext({ cwd: root })).toBe(false);
});

test("false when no project encloses the directory", async () => {
expect(await isChinaContext({ cwd: directory })).toBe(false);
});

test("false when the targets file is missing or malformed", async () => {
const noTargets = await projectWithTargets(undefined);
expect(await isChinaContext({ cwd: noTargets })).toBe(false);

await writeFile(join(noTargets, "agentcore", "aws-targets.json"), "not json");
expect(await isChinaContext({ cwd: noTargets })).toBe(false);
});
});
61 changes: 61 additions & 0 deletions src/core/partition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { readFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { partition } from "@aws-sdk/util-endpoints";

/**
* True when the region belongs to the aws-cn partition (cn-north-1,
* cn-northwest-1), per the AWS SDK's partition data rather than a name
* prefix. Features whose backing services are not reachable from that
* partition (template model providers, Bedrock Agent import, the telemetry
* collector) gate on this.
*/
export function isChinaRegion(region: string): boolean {
return partition(region).name === "aws-cn";
}

/**
* True when the CLI is operating in a China (aws-cn) context: the caller's
* resolved region (when provided — telemetry resolves through the same chain
* as withRegion) or the ambient AWS region env vars point at a China region,
* or the AgentCore project enclosing `cwd` declares a China-region deployment
* target in
* agentcore/aws-targets.json.
*
* Best-effort by design — a missing project or an unreadable/malformed targets
* file counts as non-China — so callers that must never fail (telemetry) can
* rely on it unconditionally.
*/
export async function isChinaContext(
options: { region?: string; cwd?: string } = {},
): Promise<boolean> {
const cwd = options.cwd ?? process.cwd();
if (options.region !== undefined && isChinaRegion(options.region)) return true;
if (isChinaRegion(process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? "")) return true;
Comment thread
shawnxli marked this conversation as resolved.

// Mirror project discovery: walk up to the first directory containing
// agentcore/agentcore.json, then inspect its aws-targets.json.
let dir = cwd;
while (!existsSync(join(dir, "agentcore", "agentcore.json"))) {
const parent = dirname(dir);
if (parent === dir) return false;
dir = parent;
}
try {
const raw = await readFile(join(dir, "agentcore", "aws-targets.json"), "utf8");
const targets: unknown = JSON.parse(raw);
return (
Array.isArray(targets) &&
targets.some(
(target) =>
typeof target === "object" &&
target !== null &&
"region" in target &&
typeof target.region === "string" &&
isChinaRegion(target.region),
)
);
} catch {
return false;
}
}
4 changes: 4 additions & 0 deletions src/core/project/__snapshots__/manager.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ exports[`FsProjectManager.create snapshots the Strands project manifest and runt
"build": "CodeZip",
"codeLocation": "app/agent_python_strands",
"entrypoint": "main.py",
"modelProvider": "Bedrock",
"name": "agent_python_strands",
"protocol": "HTTP",
"runtimeVersion": "PYTHON_3_14",
Expand Down Expand Up @@ -153,6 +154,7 @@ exports[`FsProjectManager.create snapshots the Strands TypeScript project manife
"build": "CodeZip",
"codeLocation": "app/agent_typescript_strands",
"entrypoint": "main.js",
"modelProvider": "Bedrock",
"name": "agent_typescript_strands",
"protocol": "HTTP",
"runtimeVersion": "NODE_22",
Expand Down Expand Up @@ -224,6 +226,7 @@ exports[`FsProjectManager.create snapshots the Strands A2A project manifest and
"build": "CodeZip",
"codeLocation": "app/a2a_python_strands",
"entrypoint": "main.py",
"modelProvider": "Bedrock",
"name": "a2a_python_strands",
"protocol": "A2A",
"runtimeVersion": "PYTHON_3_14",
Expand Down Expand Up @@ -259,6 +262,7 @@ exports[`FsProjectManager.create snapshots the LangChain project manifest and ru
"build": "CodeZip",
"codeLocation": "app/agent_python_langchain",
"entrypoint": "main.py",
"modelProvider": "Bedrock",
"name": "agent_python_langchain",
"protocol": "HTTP",
"runtimeVersion": "PYTHON_3_14",
Expand Down
Loading
Loading