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
11 changes: 7 additions & 4 deletions command.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,15 +266,18 @@ create a new project
agentcore dev [options]
```

run the project locally for development
test project changes: run Runtimes locally or deploy for Harnesses; --mode browser opens Agent Inspector.

**Options**

- `--agent <agent>`: Runtime to run
- `--agent <agent>`: runtime or harness to run
- `--ui-port <ui-port>`: port for the Agent Inspector web UI (browser mode)
- `--mode <mode>`: how to run: browser (Agent Inspector web UI) or headless (runtime output or harness invoke guidance) (default: "headless")
- `--target <target>`: deployment target whose resources local Runtimes use, or to deploy for Harnesses (default: "default" when present)
- `--port <port>`: port for the development server
- `--no-traces`: disable local OTEL trace collection
- `--mode <mode>`: how to run: browser (Agent Inspector web UI) or headless (agents stream to the terminal) (default: "headless")
- `--ui-port <ui-port>`: port for the Agent Inspector web UI (browser mode)
- `--yes`: confirm removing the target's stack when the project declares nothing to deploy (default: false)
- `--skip-deploy`: skip deployment and use the deployed harnesses (default: false)

### `agentcore deploy`

Expand Down
61 changes: 61 additions & 0 deletions src/core/project/backends/cdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1330,3 +1330,64 @@ describe("CdkBackend.resolveProjectResources", () => {
).toEqual([["gateway", "gw", ["owned", "second"]]]);
});
});

describe("CdkBackend.resolveDevEnvironment", () => {
test("reads a v0 project's deployed resource state without calling AWS", async () => {
const input = await project();
input.spec = {
...input.spec,
memories: [{ name: "history" }],
} as unknown as typeof input.spec;
input.legacy = {
format: "v0",
v0Spec: {} as never,
extras: {} as never,
};
await updateTargetState(json, input.rootPath, TARGET.name, {
resources: {
stackName: "AgentCore-example-default",
memories: {
history: {
memoryId: "memory-v0",
memoryArn: "arn:memory/memory-v0",
},
},
},
});
const subject = harness();

const env = await subject.backend.resolveDevEnvironment(input, { target: TARGET });

expect(env).toEqual({ MEMORY_HISTORY_ID: "memory-v0" });
expect(subject.stackReads).toEqual([]);
expect(subject.credentialRegions).toEqual([]);
});

test("reads a v1 project's deployed resource values from its stack outputs", async () => {
const input = await project();
input.spec = {
...input.spec,
memories: [{ name: "history" }],
} as unknown as typeof input.spec;
await updateTargetState(json, input.rootPath, TARGET.name, { stackArn: STACK_ARN });
const subject = harness({
describedStack: {
StackName: "AgentCore-example-default",
CreationTime: new Date(0),
StackStatus: "CREATE_COMPLETE",
Outputs: [
{
ExportName: "AgentCore-example-default-Memory-history-Id",
OutputValue: "memory-v1",
},
],
},
});

const env = await subject.backend.resolveDevEnvironment(input, { target: TARGET });

expect(env).toEqual({ MEMORY_HISTORY_ID: "memory-v1" });
expect(subject.stackReads).toHaveLength(1);
expect(subject.credentialRegions).toEqual([TARGET.region]);
});
});
20 changes: 20 additions & 0 deletions src/core/project/backends/cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type { AwsDeploymentTarget } from "../../../projectSchemas/aws-targets";
import type {
DeployBackendInput,
ProjectBackend,
ResolveDevEnvironmentBackendInput,
ResolveDeployedResourcesBackendInput,
ResolveProjectResourcesBackendInput,
} from "./types";
Expand Down Expand Up @@ -75,6 +76,7 @@ import {
type PaymentConnectorAuthorizationUrlReporter,
} from "./cdk/paymentConnectorAuthorizationUrls";
import { describeStack } from "./cdk/stackReader";
import { projectDevEnvironment } from "./cdk/devEnvironment";

type StackDescriber = typeof describeStack;

Expand Down Expand Up @@ -667,6 +669,24 @@ export class CdkBackend implements ProjectBackend {
];
}

public async resolveDevEnvironment(
project: Project,
input: ResolveDevEnvironmentBackendInput,
): Promise<Record<string, string>> {
const deployedState = await readDeployedState(this.json, project.rootPath);
const recorded = deployedState.targets[input.target.name];
const stackReference = stackReferenceOf(recorded);
const stack =
stackReference && !project.legacy
? await this.describeStack(
input.target.region,
await this.credentialsForTarget(input.target),
stackReference,
)
: undefined;
return projectDevEnvironment({ project, recorded, stack: stack ?? undefined });
}

private async credentialsForTarget(target: AwsDeploymentTarget) {
const credentials = await this.resolveCredentials(target.region);
const account = await this.resolveAccount(target.region, credentials);
Expand Down
232 changes: 232 additions & 0 deletions src/core/project/backends/cdk/devEnvironment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import { describe, expect, test } from "bun:test";
import type { Stack } from "@aws-sdk/client-cloudformation";
import type { Project } from "../../../../handlers/project/types";
import { ProjectSpecSchema } from "../../../../projectSchemas/project";
import type { TargetState } from "./deployedState";
import { projectDevEnvironment } from "./devEnvironment";

function project(overrides: Partial<Project["spec"]> = {}): Project {
return {
name: "orders",
rootPath: "/workspace/orders",
spec: {
...ProjectSpecSchema.parse({ name: "orders", version: 2 }),
...overrides,
},
};
}

const configuredProject = project({
memories: [{ name: "order_history" }],
agentCoreGateways: [
{
name: "tools-gateway",
authorizerType: "AWS_IAM",
protocolType: "MCP",
targets: [],
},
],
payments: [
{
name: "Billing",
authorizerType: "CUSTOM_JWT",
connectors: [
{
name: "primary_wallet",
provider: "CoinbaseCDP",
provisionMode: "QUICK_CREATE",
},
],
autoPayment: true,
defaultSpendLimit: "10.00",
paymentToolAllowlist: ["pay"],
networkPreferences: ["eip155:1"],
},
],
} as unknown as Partial<Project["spec"]>);

describe("projectDevEnvironment", () => {
test("reads the v0 memory, gateway, and payment state", () => {
const recorded = {
resources: {
memories: {
order_history: {
memoryId: "memory-1",
memoryArn: "arn:memory/memory-1",
},
},
mcp: {
gateways: {
"tools-gateway": {
gatewayId: "gateway-1",
gatewayArn: "arn:gateway/gateway-1",
gatewayUrl: "https://gateway.example.test",
},
},
},
payments: {
Billing: {
managerId: "billing-1",
managerArn: "arn:payment/billing-1",
processPaymentRoleArn: "arn:role/process-payment",
resourceRetrievalRoleArn: "arn:role/resource-retrieval",
authorizerType: "AWS_IAM",
autoPayment: false,
paymentToolAllowlist: ["refund"],
networkPreferences: ["eip155:8453"],
connectors: {
primary_wallet: {
connectorId: "connector-1",
provisionMode: "QUICK_CREATE",
},
},
},
},
},
} as TargetState;

expect(projectDevEnvironment({ project: configuredProject, recorded })).toEqual({
MEMORY_ORDER_HISTORY_ID: "memory-1",
AGENTCORE_GATEWAY_TOOLS_GATEWAY_URL: "https://gateway.example.test",
AGENTCORE_GATEWAY_TOOLS_GATEWAY_AUTH_TYPE: "AWS_IAM",
AGENTCORE_PAYMENT_BILLING_MANAGER_ARN: "arn:payment/billing-1",
AGENTCORE_PAYMENT_BILLING_PROCESS_PAYMENT_ROLE_ARN: "arn:role/process-payment",
AGENTCORE_PAYMENT_BILLING_CONNECTOR_ID: "connector-1",
AGENTCORE_PAYMENT_BILLING_AUTO_PAYMENT: "false",
AGENTCORE_PAYMENT_BILLING_TOOL_ALLOWLIST: "refund",
AGENTCORE_PAYMENT_BILLING_NETWORK_PREFERENCES: "eip155:8453",
});
});

test("keeps discovering v0 resources that are only present in deployed state", () => {
const recorded = {
resources: {
memories: {
"removed-memory": {
memoryId: "memory-legacy",
},
},
mcp: {
gateways: {
"removed-gateway": {
gatewayUrl: "https://legacy-gateway.example.test",
},
},
},
gateways: {
"removed-gateway": {
gatewayUrl: "https://stale-layout.example.test",
},
},
payments: {
"removed-payment": {
managerArn: "arn:payment/legacy",
connectors: {
"first-connector": { connectorId: "connector-first" },
"second-connector": { connectorId: "connector-second" },
},
paymentToolAllowlist: [],
networkPreferences: [],
},
},
},
} as TargetState;

expect(projectDevEnvironment({ project: project(), recorded })).toEqual({
MEMORY_REMOVED_MEMORY_ID: "memory-legacy",
AGENTCORE_GATEWAY_REMOVED_GATEWAY_URL: "https://legacy-gateway.example.test",
AGENTCORE_GATEWAY_REMOVED_GATEWAY_AUTH_TYPE: "NONE",
AGENTCORE_PAYMENT_REMOVED_PAYMENT_MANAGER_ARN: "arn:payment/legacy",
AGENTCORE_PAYMENT_REMOVED_PAYMENT_CONNECTOR_ID: "connector-first",
});
});

test("falls back to v1 CloudFormation outputs", () => {
const stack = {
StackName: "AgentCore-orders-default",
Outputs: [
{
ExportName: "AgentCore-orders-default-Memory-order-history-Id",
OutputValue: "memory-2",
},
{
ExportName: "AgentCore-orders-default-Gateway-tools-gateway-Url",
OutputValue: "https://v1-gateway.example.test",
},
{ OutputKey: "PaymentBillingManagerArn", OutputValue: "arn:payment/billing-2" },
{
OutputKey: "PaymentBillingProcessPaymentRoleArn",
OutputValue: "arn:role/process-payment-2",
},
{
OutputKey: "PaymentBillingprimarywalletConnectorId",
OutputValue: "connector-2",
},
],
} as Stack;

expect(projectDevEnvironment({ project: configuredProject, stack })).toEqual({
MEMORY_ORDER_HISTORY_ID: "memory-2",
AGENTCORE_GATEWAY_TOOLS_GATEWAY_URL: "https://v1-gateway.example.test",
AGENTCORE_GATEWAY_TOOLS_GATEWAY_AUTH_TYPE: "AWS_IAM",
AGENTCORE_PAYMENT_BILLING_MANAGER_ARN: "arn:payment/billing-2",
AGENTCORE_PAYMENT_BILLING_PROCESS_PAYMENT_ROLE_ARN: "arn:role/process-payment-2",
AGENTCORE_PAYMENT_BILLING_CONNECTOR_ID: "connector-2",
AGENTCORE_PAYMENT_BILLING_AUTO_PAYMENT: "true",
AGENTCORE_PAYMENT_BILLING_TOOL_ALLOWLIST: "pay",
AGENTCORE_PAYMENT_BILLING_NETWORK_PREFERENCES: "eip155:1",
AGENTCORE_PAYMENT_BILLING_AUTH_MODE: "bearer",
});
});

test("omits empty v1 payment list settings", () => {
const withoutLists = project({
payments: [
{
name: "Billing",
authorizerType: "AWS_IAM",
connectors: [],
autoPayment: false,
defaultSpendLimit: "10.00",
paymentToolAllowlist: [],
networkPreferences: [],
},
],
} as unknown as Partial<Project["spec"]>);
const stack = {
StackName: "AgentCore-orders-default",
Outputs: [{ OutputKey: "PaymentBillingManagerArn", OutputValue: "arn:payment/billing" }],
} as Stack;

expect(projectDevEnvironment({ project: withoutLists, stack })).toEqual({
AGENTCORE_PAYMENT_BILLING_MANAGER_ARN: "arn:payment/billing",
AGENTCORE_PAYMENT_BILLING_AUTO_PAYMENT: "false",
});
});

test("rejects resource names that collide after environment sanitization", () => {
const colliding = project({
agentCoreGateways: [
{ name: "Tools", authorizerType: "NONE", protocolType: "MCP", targets: [] },
{ name: "tools", authorizerType: "NONE", protocolType: "MCP", targets: [] },
],
} as unknown as Partial<Project["spec"]>);
const stack = {
StackName: "AgentCore-orders-default",
Outputs: [
{
ExportName: "AgentCore-orders-default-Gateway-Tools-Url",
OutputValue: "https://first.example.test",
},
{
ExportName: "AgentCore-orders-default-Gateway-tools-Url",
OutputValue: "https://second.example.test",
},
],
} as Stack;

expect(() => projectDevEnvironment({ project: colliding, stack })).toThrow(
"both map to environment variable AGENTCORE_GATEWAY_TOOLS_URL",
);
});
});
Loading
Loading