Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4fc2445
feat(cdk): add reproducible stack census and stability audit (#852)
Sep 17, 2026
ed7b8eb
fix(cdk): stabilize input guardrail version identity (#852)
Sep 17, 2026
fa428e7
feat(cdk): add staged blueprint controller handoff (#852)
Sep 18, 2026
988544a
fix(cdk): isolate agent image build inputs (#852)
Sep 18, 2026
2209c00
fix(cdk): scope gateway and vault IAM audit annotations (#852)
Sep 18, 2026
74f5792
feat(cdk): retain stateful resources before stack decomposition (#852)
Sep 21, 2026
ac18c28
feat(cdk): select exclusive compute and enforce deployment budgets (#…
Sep 21, 2026
566a69a
Merge remote-tracking branch 'origin/main' into docs/852-stack-decomp…
Sep 21, 2026
4130f4e
feat(cdk): extract an optional network stack (#852)
Sep 21, 2026
e283624
test(cdk): consolidate deployment budget and permission guards (#852)
Sep 21, 2026
8720df2
Merge origin/main into docs/852-stack-decomposition-research (#852)
Sep 22, 2026
67e925c
fix(cdk): enforce resource budgets for operator AZ overrides (#852)
Sep 22, 2026
7212340
fix(cdk): preserve logs and subnet addresses across transitions
Sep 22, 2026
2b2bee3
Merge branch 'main' into docs/852-stack-decomposition-research
isadeks Oct 1, 2026
00dcf8b
Merge branch 'main' into docs/852-stack-decomposition-research
isadeks Oct 2, 2026
18d8dba
Merge latest main into PR #912
Oct 2, 2026
9863ad3
feat(cdk): select one or more compute backends with compute_types
isadeks Oct 2, 2026
9327dbc
test(cdk): keep census workspace fixtures out of the hook's repository
isadeks Oct 2, 2026
2095401
fix(cdk): narrow PR 912 to compatible compute and network budgets
Oct 2, 2026
362043c
fix(cdk): audit expanded model grants through policy overflow
Oct 5, 2026
c6251d5
Merge remote-tracking branch 'origin/main' into fix/852-pr912-review
Oct 5, 2026
61a1818
fix(cdk): keep Linear vault audit exceptions account scoped
Oct 5, 2026
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: 11 additions & 0 deletions agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -481,3 +481,14 @@ agent/
The container **CMD** runs the app under `opentelemetry-instrument` with **uvicorn** using the **asyncio** event loop (not uvloop), avoiding known subprocess issues with uvloop.

**Diagnostics:** `scripts/diagnostics/` holds optional smoke tests for local AgentCore debugging. They are not copied into the production Docker image.

### MicroVM optional service configuration

The shared `contracts/constants.json` allowlist transports `tool_gateway_url` →
`ABCA_TOOL_GATEWAY_URL`, `linear_vault_enabled` → `LINEAR_VAULT_ENABLED`, and
`linear_workload_identity_name` → `LINEAR_WORKLOAD_IDENTITY_NAME` in the MicroVM
`platform_config` payload. These optional values come from the deployed Gateway
and vault configuration. They contain identifiers, not credential values.
Rebuild the MicroVM snapshot from this checkout before enabling those features;
an older image does not recognize the new keys. AgentCore and ECS receive the
same settings through their deployment environment.
5 changes: 4 additions & 1 deletion agent/tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1824,6 +1824,9 @@ def test_wire_contract_is_exactly_the_documented_key_set(self):
# wrong model that the IAM grant does not cover on any non-``global``
# deployment, surfacing as AccessDenied at turn 0.
"anthropic_model": "ANTHROPIC_MODEL",
"tool_gateway_url": "ABCA_TOOL_GATEWAY_URL",
"linear_vault_enabled": "LINEAR_VAULT_ENABLED",
"linear_workload_identity_name": "LINEAR_WORKLOAD_IDENTITY_NAME",
}

def test_required_subset_is_exactly_the_four_run_blocking_keys(self):
Expand Down Expand Up @@ -2005,7 +2008,7 @@ def test_every_allowlisted_key_is_installable(self, env_guard):
key: _platform_config_value(key) for key in server.MICROVM_PLATFORM_CONFIG_ENV_BY_KEY
}
installed = server._install_platform_config(full)
assert installed == sorted(server.MICROVM_PLATFORM_CONFIG_ENV_BY_KEY.values())
assert sorted(installed) == sorted(server.MICROVM_PLATFORM_CONFIG_ENV_BY_KEY.values())
for key, env_name in server.MICROVM_PLATFORM_CONFIG_ENV_BY_KEY.items():
assert os.environ[env_name] == _platform_config_value(key)

Expand Down
1 change: 1 addition & 0 deletions cdk/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ beforeAll(() => {

## Common mistakes

- **API Gateway Lambda permission growth** — Pass `allowTestInvoke: false` on every new `LambdaIntegration` and keep `scopePermissionToMethod` at its default `true`. `test/synthesis/deployment.test.ts` checks permissions and template budgets across the full deployment profile product, including nested stacks.
- **Lambda bundling in unit tests** — `Template.fromStack()` synths the stack but bundling is disabled via `CDK_CONTEXT_JSON`. Do not re-enable globally; opt in per-test with `postCliContext` only when asserting on bundle output. Details: `test/setup/disable-bundling.ts`, #366.
- **Cedar engine drift** — `@cedar-policy/cedar-wasm` and `cedarpy` share a Rust core. Bump both + parity fixtures in one commit. See `docs/design/CEDAR_HITL_GATES.md` §15.6 and `mise.toml` parity banner.
- **Types out of sync** — `cdk/src/handlers/shared/types.ts` and `cli/src/types.ts` must match; CI runs `check-types-sync`.
Expand Down
5 changes: 5 additions & 0 deletions cdk/mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ run = "yarn jest --coverage=false --runInBand"
description = "cdk synth"
run = ["mkdir -p $TMPDIR", "yarn synth"]

[tasks.census]
description = "Measure named CDK profiles and optionally audit synthesis stability (offline, unbundled)"
depends = [":compile"]
run = "yarn census"

[tasks."synth:quiet"]
description = "cdk synth (quiet)"
depends = [":compile"]
Expand Down
3 changes: 2 additions & 1 deletion cdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"test": "jest --maxWorkers=${JEST_MAX_WORKERS:-25%}",
"eslint": "eslint --fix src test",
"synth": "npx cdk synth",
"synth:quiet": "npx cdk synth -q"
"synth:quiet": "npx cdk synth -q",
"census": "node -r ts-node/register/transpile-only src/synthesis/cli.ts"
},
"dependencies": {
"@aws-cdk/aws-bedrock-alpha": "2.260.0-alpha.0",
Expand Down
58 changes: 58 additions & 0 deletions cdk/src/blueprints/definitions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* MIT No Attribution
*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

import type { Node } from 'constructs';
import type { BlueprintProps } from '../constructs/blueprint';

/** Repository configuration before it is bound to a RepoTable or a stack. */
export interface BlueprintDefinition extends Omit<BlueprintProps, 'repoTable'> {
/** Stable construct ID for this repository's provisioning controller. */
readonly id: string;
}

/** Resolve once so repository provisioning and the network use the same inputs. */
export function resolveBlueprintDefinitions(
node: Node,
environment: NodeJS.ProcessEnv = process.env,
): readonly BlueprintDefinition[] {
const definitions: BlueprintDefinition[] = [{
id: 'AgentPluginsBlueprint',
repo: environment.BLUEPRINT_REPO ?? node.tryGetContext('blueprintRepo') ?? 'awslabs/agent-plugins',
}];
// Optional per-repository registry assets (#246); preserve the deployed IDs
// and environment/context precedence while moving configuration out of a stack.
const forkRepo = environment.FORK_BLUEPRINT_REPO ?? node.tryGetContext('forkBlueprintRepo');
if (forkRepo) {
definitions.push({
id: 'ForkBlueprint',
repo: forkRepo,
assets: {
mcpServers: ['registry://mcp_server/acme/aws-knowledge@^1.0.0'],
cedarPolicyModules: ['registry://cedar_policy_module/acme/guard@^1.0.0'],
skills: ['registry://skill/acme/readme-helper@^1.0.0'],
},
});
}
return definitions;
}

/** Aggregate plain domain strings without referring to repository resources. */
export function blueprintEgressDomains(definitions: readonly BlueprintDefinition[]): string[] {
return [...new Set(definitions.flatMap(definition => definition.networking?.egressAllowlist ?? []))];
}
67 changes: 43 additions & 24 deletions cdk/src/constructs/agent-session-role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@
*/

import * as bedrock from '@aws-cdk/aws-bedrock-alpha';
import { Duration } from 'aws-cdk-lib';
import { AspectPriority, Aspects, Duration, Lazy } from 'aws-cdk-lib';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { NagSuppressions } from 'cdk-nag';
import { Construct } from 'constructs';
import { Construct, IConstruct } from 'constructs';

/** S3 key prefixes the agent writes/reads, scoped per tenant. */
const TRACE_KEY_PREFIX = 'traces';
Expand All @@ -42,7 +42,9 @@ export interface AgentSessionRoleProps {
* the trust surface. Both run the same trusted agent code, which sources the
* `{user_id, repo, task_id}` tag values from the resolved TaskConfig.
*/
readonly assumingRoles: iam.IRole[];
readonly assumingRoles?: iam.IRole[];
/** Admit deployed backends with admitComputeRole after constructing this role. */
readonly deferComputeRoleBinding?: boolean;

/**
* The four task-scoped DynamoDB tables, all partitioned by `task_id`. The
Expand Down Expand Up @@ -125,24 +127,35 @@ export class AgentSessionRole extends Construct {

/** The SessionRole. Assumed by the agent at task startup. */
public readonly role: iam.Role;
private readonly admittedRoles = new Set<iam.IRole>();

constructor(scope: Construct, id: string, props: AgentSessionRoleProps) {
super(scope, id);

if (props.assumingRoles.length === 0) {
if (!props.assumingRoles?.length && !props.deferComputeRoleBinding) {
// A SessionRole no principal can assume is dead weight and would
// synthesize an empty/invalid trust policy. Fail at synth instead.
throw new Error(
'AgentSessionRole requires at least one assuming role (the compute role[s] that mint scoped credentials)',
);
}

const [firstAssumingRole] = props.assumingRoles;
if (props.assumingRoles?.length && props.deferComputeRoleBinding) {
throw new Error('Specify assumingRoles or deferComputeRoleBinding, not both');
}
this.node.addValidation({ validate: () => this.admittedRoles.size ? [] : ['AgentSessionRole requires an admitted compute role before synthesis'] });
const firstAssumingRoleArn = props.assumingRoles?.[0]?.roleArn ?? Lazy.string({
produce: () => {
const first = this.admittedRoles.values().next().value;
if (!first) throw new Error('AgentSessionRole requires an admitted compute role before synthesis');
return first.roleArn;
},
});

// CDK requires assumedBy; additional principals are admitted via
// admitComputeRole so trust + grant always wire together.
this.role = new iam.Role(this, 'Role', {
assumedBy: new iam.ArnPrincipal(firstAssumingRole.roleArn),
assumedBy: new iam.ArnPrincipal(firstAssumingRoleArn),
description:
'Per-task scoped credentials for ABCA agent tenant-data access '
+ '(DynamoDB task rows + S3 trace/attachment objects), constrained by '
Expand Down Expand Up @@ -226,30 +239,34 @@ export class AgentSessionRole extends Construct {
invokable.grantInvoke(this.role);
}

// The object-level prefix conditions above already constrain access to the
// session's own tenant prefix; the remaining wildcard is the per-object
// suffix (task_id/attachment_id/filename), which is the intended scope.
NagSuppressions.addResourceSuppressions(
this.role,
[
{
// Model-list overrides can spill these grants into managed policies created
// during synthesis. Visit every policy before cdk-nag, including that late
// overflow, and allow only the wildcard shapes this construct requires.
Aspects.of(this.role).add({
visit(node: IConstruct): void {
if (!(node instanceof iam.CfnRole || node instanceof iam.CfnPolicy || node instanceof iam.CfnManagedPolicy)) return;
NagSuppressions.addResourceSuppressions(node, [{
id: 'AwsSolutions-IAM5',
reason:
'Resource wildcards are the per-object suffix under a tenant-scoped '
+ 'prefix (traces/${aws:PrincipalTag/user_id}/*, '
+ 'attachments/${aws:PrincipalTag/user_id}/*, '
+ 'artifacts/${aws:PrincipalTag/task_id}/*) and the DynamoDB item '
+ 'set gated by a dynamodb:LeadingKeys = ${aws:PrincipalTag/task_id} '
+ 'condition — narrower than the compute role this replaces. Bedrock '
+ 'InvokeModel resources are the explicit model + inference-profile '
+ 'ARNs from grantInvoke (cross-region profiles fan out to per-region '
+ 'foundation-model ARNs), matching the compute role grant (#215).',
},
],
true,
);
+ 'artifacts/${aws:PrincipalTag/task_id}/*). Bedrock grantInvoke uses '
+ 'InvokeModel* for synchronous/streaming invocation and a region '
+ 'wildcard for each literal foundation-model ID routed by a '
+ 'cross-region inference profile, matching the compute role (#215).',
appliesTo: [
// cdk-nag renders policy variables as <name> in finding IDs.
{ regex: '/^Resource::[^*?]+/(traces|attachments)/<aws:PrincipalTag/user_id>/\\*$/' },
{ regex: '/^Resource::[^*?]+/artifacts/<aws:PrincipalTag/task_id>/\\*$/' },
'Action::bedrock:InvokeModel*',
{ regex: '/^Resource::arn:[^*?]+:bedrock:\\*::foundation-model/[^*?]+$/' },
],
}]);
},
}, { priority: AspectPriority.MUTATING });

for (const computeRole of props.assumingRoles) {
for (const computeRole of props.assumingRoles ?? []) {
this.admitComputeRole(computeRole);
}
}
Expand All @@ -260,6 +277,8 @@ export class AgentSessionRole extends Construct {
* `sts:AssumeRole`/`sts:TagSession` on the compute role's identity policy.
*/
public admitComputeRole(computeRole: iam.IRole): void {
if (this.admittedRoles.has(computeRole)) return;
this.admittedRoles.add(computeRole);
this.addTrustForComputeRole(computeRole);
this.grantAssumeToComputeRole(computeRole);
}
Expand Down
60 changes: 55 additions & 5 deletions cdk/src/constructs/agent-vpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* SOFTWARE.
*/

import { RemovalPolicy } from 'aws-cdk-lib';
import { RemovalPolicy, Tags } from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as logs from 'aws-cdk-lib/aws-logs';
import { NagSuppressions } from 'cdk-nag';
Expand All @@ -37,6 +37,26 @@ const DEFAULT_AGENT_VPC_AZS = 2;
/** AgentCore high-availability floor: at least two zones. */
const MIN_AGENT_VPC_AZS = 2;

const MAX_RESERVED_NETWORK_AZS = 6;

/** Reserve unused AZ address slots without allocating subnets or other resources. */
export function resolveNetworkReservedAzs(value: unknown): number {
if (value === undefined) return 0;
const count = typeof value === 'string' && value.trim() !== '' ? Number(value) : value;
// Six slots cover the largest current regional AZ count and bound CDK's
// placeholder allocation. The normal three-to-two-zone reduction needs one.
if (typeof count !== 'number' || !Number.isInteger(count) || count < 0 || count > MAX_RESERVED_NETWORK_AZS) {
throw new Error(`networkReservedAzs must be an integer from 0 to ${MAX_RESERVED_NETWORK_AZS}`);
}
return count;
}

/** The references consumed by any compute backend, regardless of stack ownership. */
export interface AgentNetwork {
readonly vpc: ec2.IVpc;
readonly runtimeSecurityGroup: ec2.ISecurityGroup;
}

/**
* Properties for the AgentVpc construct.
*/
Expand Down Expand Up @@ -94,6 +114,13 @@ export interface AgentVpcProps {
* @default RemovalPolicy.DESTROY
*/
readonly removalPolicy?: RemovalPolicy;

/**
* Original AgentVpc construct path used for generated Name tags and endpoint
* security-group descriptions. Keeps service properties stable across a move.
* @default - this construct's current path
*/
readonly resourcePath?: string;
}

/**
Expand All @@ -103,7 +130,7 @@ export interface AgentVpcProps {
* and NAT for internet egress (GitHub and package registries).
* Flow logs are enabled for audit.
*/
export class AgentVpc extends Construct {
export class AgentVpc extends Construct implements AgentNetwork {
/** The VPC where the Runtime will be deployed. */
public readonly vpc: ec2.Vpc;

Expand Down Expand Up @@ -134,14 +161,20 @@ export class AgentVpc extends Construct {
const maxAzs = props.maxAzs ?? DEFAULT_AGENT_VPC_AZS;
const natGateways = props.natGateways ?? 1;
const removalPolicy = props.removalPolicy ?? RemovalPolicy.DESTROY;
const reservedAzs = resolveNetworkReservedAzs(this.node.tryGetContext('networkReservedAzs'));

// --- VPC ---
// When explicit AZs are provided (to target AgentCore-supported physical
// zones), pass them directly and omit maxAzs — CDK does not allow both.
this.vpc = new ec2.Vpc(this, 'Vpc', {
...(pinnedAzs?.length
? { availabilityZones: pinnedAzs }
// CDK appends reserved placeholders to this array; keep the caller's
// real AZ selection intact for application wiring and diagnostics.
? { availabilityZones: [...pinnedAzs] }
: { maxAzs }),
// Keep active + reserved slots constant during an AZ reduction so CDK's
// private subnet CIDRs do not shift and force subnet replacements.
reservedAzs,
natGateways,
restrictDefaultSecurityGroup: true,
subnetConfiguration: [
Expand All @@ -158,16 +191,27 @@ export class AgentVpc extends Construct {
],
});

const resourceVpcPath = `${props.resourcePath ?? this.node.path}/Vpc`;
if (props.resourcePath !== undefined) {
// CDK gives the VPC and each subnet their own inherited Name tag. Preserve
// those scopes so routes, NAT, endpoints and the IGW keep their old names.
Tags.of(this.vpc).add('Name', resourceVpcPath);
for (const subnet of [...this.vpc.publicSubnets, ...this.vpc.privateSubnets]) {
Tags.of(subnet).add('Name', `${resourceVpcPath}${subnet.node.path.slice(this.vpc.node.path.length)}`);
}
}

// --- Flow logs (satisfies AwsSolutions-VPC7) ---
const flowLogGroup = new logs.LogGroup(this, 'FlowLogGroup', {
retention: logs.RetentionDays.ONE_MONTH,
removalPolicy,
});

this.vpc.addFlowLog('FlowLog', {
const flowLog = this.vpc.addFlowLog('FlowLog', {
destination: ec2.FlowLogDestination.toCloudWatchLogs(flowLogGroup),
trafficType: ec2.FlowLogTrafficType.ALL,
});
if (props.resourcePath !== undefined) Tags.of(flowLog).add('Name', `${resourceVpcPath}/FlowLog`);

NagSuppressions.addResourceSuppressions(this.vpc, [
{
Expand Down Expand Up @@ -210,11 +254,17 @@ export class AgentVpc extends Construct {
];

for (const ep of interfaceEndpoints) {
this.vpc.addInterfaceEndpoint(ep.id, {
const endpoint = this.vpc.addInterfaceEndpoint(ep.id, {
service: ep.service,
privateDnsEnabled: true,
subnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
});
if (props.resourcePath !== undefined) {
// GroupDescription is replacement-sensitive. The CDK default includes
// the current stack path, so explicitly keep the pre-extraction value.
const group = endpoint.node.findChild('SecurityGroup').node.defaultChild as ec2.CfnSecurityGroup;
group.groupDescription = `${resourceVpcPath}/${ep.id}/SecurityGroup`;
}
}
}
}
3 changes: 2 additions & 1 deletion cdk/src/constructs/ecs-agent-cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ export class EcsAgentCluster extends Construct {
public readonly securityGroup: ec2.SecurityGroup;
public readonly containerName: string;
public readonly taskRoleArn: string;
public readonly logGroup: logs.LogGroup;
public readonly executionRoleArn: string;

constructor(scope: Construct, id: string, props: EcsAgentClusterProps) {
Expand Down Expand Up @@ -349,7 +350,7 @@ export class EcsAgentCluster extends Construct {
);

// CloudWatch log group for agent task output
const logGroup = new logs.LogGroup(this, 'TaskLogGroup', {
const logGroup = this.logGroup = new logs.LogGroup(this, 'TaskLogGroup', {
retention: logs.RetentionDays.THREE_MONTHS,
removalPolicy: RemovalPolicy.DESTROY,
});
Expand Down
Loading
Loading