Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 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
91 changes: 27 additions & 64 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,67 +1,30 @@
# Build context is repo root (see cdk/src/stacks/agent.ts) so the
# Dockerfile can COPY contracts/ alongside agent/. Exclusions below
# keep the context lean — without them the entire monorepo (CDK
# cdk.out/, node_modules/, docs/dist/, etc.) gets uploaded on every
# AgentCore deploy.

# CDK output (recursive include if not excluded)
cdk/cdk.out/
cdk/lib/
cdk/node_modules/

# CLI and docs build artifacts
cli/lib/
cli/node_modules/
docs/dist/
docs/node_modules/
docs/.astro/

# Shared node_modules
node_modules/

# Agent venv and cache (rebuilt inside image via uv)
agent/.venv/
agent/__pycache__/
agent/**/__pycache__/
agent/**/*.pyc

# Git and tooling
.git/
.prek/
.claude/
**/.DS_Store

# Docs and assets not needed in image
*.md
*.png
*.drawio
*.html
*.gif
*.tape

# Worktrees + scratch
abca-worktrees/
.next-session-prompt.md
.e2e-test-plan.md

# Test/coverage output
coverage/
**/coverage/
.pytest_cache/
# The AgentCore and ECS images use the repository root with agent/Dockerfile.
# Admit only the Dockerfile's local COPY inputs and build-control files.
# Keep this list aligned with COPY when adding a runtime input; the CDK image
# context tests verify preservation and exclude unrelated/generated files.
**
!.dockerignore
!agent/
agent/**
!agent/Dockerfile
!agent/pyproject.toml
!agent/uv.lock
!agent/prepare-commit-msg.sh
!agent/managed-settings.json
!agent/src/
!agent/src/**
!agent/policies/
!agent/policies/**
!agent/workflows/
!agent/workflows/**
!contracts/
!contracts/**

# Generated files can occur inside an admitted runtime directory too.
**/__pycache__/
**/*.pyc
**/.pytest_cache/
# Coverage data FILES, not just the directories above. pytest-cov writes
# per-process temp files (``.coverage.<host>.<pid>.<random>``) and deletes them as
# it combines. The agent test suite and the CDK suite run in parallel, and the CDK
# suite fingerprints this tree for the agent image asset — so a file that vanishes
# mid-walk fails the build with ENOENT on a path nothing will ever read again.
.coverage
.coverage.*
**/.ruff_cache/
**/.DS_Store
**/.coverage
**/.coverage.*

# IDE / OS
.idea/
.vscode/
yarn-error.log
yarn-debug.log
npm-debug.log*
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
68 changes: 68 additions & 0 deletions cdk/src/blueprints/configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* 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 { AttributeValue } from '@aws-sdk/client-dynamodb';

export const REPO_PATTERN = /^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/;
export const ASSET_FIELDS = ['mcp_servers', 'cedar_policy_modules', 'skills'] as const;
export const CONFIGURATION_FIELDS = {
compute_type: 'S',
runtime_arn: 'S',
model_id: 'S',
max_turns: 'N',
max_budget_usd: 'N',
system_prompt_overrides: 'S',
github_token_secret_arn: 'S',
poll_interval_ms: 'N',
build_command: 'S',
lint_command: 'S',
egress_allowlist: 'L',
cedar_policies: 'L',
approval_gate_cap: 'N',
mcp_servers: 'L',
cedar_policy_modules: 'L',
skills: 'L',
} as const;
export type BlueprintConfiguration = Partial<Record<keyof typeof CONFIGURATION_FIELDS, AttributeValue>>;
export type BlueprintProvisioningMode = 'legacy' | 'prepare' | 'adopt' | 'managed';

export function blueprintProvisioningMode(value: unknown): BlueprintProvisioningMode {
if (value === undefined) return 'legacy';
if (value === 'legacy' || value === 'prepare' || value === 'adopt' || value === 'managed') return value;
throw new Error('blueprintProvisioning must be legacy, prepare, adopt, or managed');
}

/** These are only the fields owned by a Blueprint, never arbitrary DynamoDB attributes. */
export function parseConfiguration(json: string): BlueprintConfiguration {
const parsed: unknown = JSON.parse(json);
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('Invalid blueprint configuration');
for (const [key, value] of Object.entries(parsed)) {
const kind = CONFIGURATION_FIELDS[key as keyof typeof CONFIGURATION_FIELDS];
if (!Object.hasOwn(CONFIGURATION_FIELDS, key) || !value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).length !== 1) {
throw new Error(`Invalid blueprint configuration field: ${key}`);
}
const attribute = value as Record<string, unknown>;
const valid = kind === 'S' ? typeof attribute.S === 'string'
: kind === 'N' ? typeof attribute.N === 'string' && /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(attribute.N) && Number.isFinite(Number(attribute.N))
: Array.isArray(attribute.L) && attribute.L.every(v =>
v && typeof v === 'object' && Object.keys(v).length === 1 && typeof v.S === 'string');
if (!valid) throw new Error(`Invalid blueprint configuration value for ${key}`);
}
return parsed as BlueprintConfiguration;
}
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 ?? []))];
}
27 changes: 21 additions & 6 deletions cdk/src/constructs/agent-session-role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/

import * as bedrock from '@aws-cdk/aws-bedrock-alpha';
import { Duration } from 'aws-cdk-lib';
import { 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';
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 the selected backend 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 @@ -249,7 +262,7 @@ export class AgentSessionRole extends Construct {
true,
);

for (const computeRole of props.assumingRoles) {
for (const computeRole of props.assumingRoles ?? []) {
this.admitComputeRole(computeRole);
}
}
Expand All @@ -260,6 +273,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
Loading
Loading