Skip to content
Open
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
9 changes: 5 additions & 4 deletions cdk/src/constructs/bedrock-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,11 @@ const GEO_PREFIX_RE = new RegExp(`^(?:${GEO_ALTERNATION})\\.`);
* redeploy, to move the deployment's inference profiles to another geography —
* no construct edits needed.
*
* Both grant sites derive their inference-profile ARNs from this one value (the
* All three grant sites derive their inference-profile ARNs from this one value (the
* AgentCore runtime in `stacks/agent.ts` via
* `CrossRegionInferenceProfile.fromConfig`, the ECS task role in
* `constructs/ecs-agent-cluster.ts` via the `<geo>.<modelId>` ARN resource
* `CrossRegionInferenceProfile.fromConfig`, and the ECS task role in
* `constructs/ecs-agent-cluster.ts` and MicroVM execution role in
* `constructs/lambda-microvm-compute.ts` via the `<geo>.<modelId>` ARN resource
* name), and the agent's auxiliary-model env var (`ANTHROPIC_DEFAULT_HAIKU_MODEL`)
* takes the same prefix via {@link inferenceProfileId} — so the main and
* auxiliary models can never route through different geographies.
Expand Down Expand Up @@ -138,7 +139,7 @@ export function resolveBedrockGeoRegion(node: Node): CrossRegionInferenceProfile
* a model the runtime may invoke — no construct edits needed.
*
* **Use the bare foundation-model ID (`anthropic.claude-…`), NOT a
* geo-prefixed inference-profile ID.** Both grant sites derive the
* geo-prefixed inference-profile ID.** All three grant sites derive the
* inference-profile ARN by prefixing the geography from
* {@link resolveBedrockGeoRegion} (`bedrockGeoRegion` — `global` in the shipped cdk.json, `us` absent any context), so passing
* `us.anthropic.…` here would produce an invalid `us.us.anthropic.…` ARN. The
Expand Down
9 changes: 5 additions & 4 deletions cdk/src/constructs/lambda-microvm-compute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import { Construct } from 'constructs';
// costs nothing and cannot drift.
import { AgentMemory } from './agent-memory';
import { AgentSessionRole } from './agent-session-role';
import { resolveBedrockModelIds } from './bedrock-models';
import { inferenceProfileId, resolveBedrockGeoRegion, resolveBedrockModelIds } from './bedrock-models';
import sharedConstants from '../../../contracts/constants.json';
import { LAMBDA_MICROVM_SUPPORTED_REGIONS, isLambdaMicrovmRegionSupported } from '../handlers/shared/microvm-regions';

Expand Down Expand Up @@ -1190,14 +1190,15 @@ export class LambdaMicrovmCompute extends Construct {
// Bedrock model invocation — scoped to explicit foundation-model and
// cross-Region inference-profile ARNs (parity with the AgentCore runtime and
// the ECS task role), NEVER `Resource: '*'`. The model set comes from the
// shared, context-overridable list (`constructs/bedrock-models.ts`) so no
// backend can drift from the others.
// shared, context-overridable list (`constructs/bedrock-models.ts`). Resolve
// the same geography as the agent's model IDs so grants and calls cannot drift.
//
// Required on the COMPUTE role even though the SessionRole carries a
// session-tagged Bedrock grant for cost attribution (#215): that attribution
// is designed to FAIL OPEN — Claude Code's credential helper falls back to
// ambient compute-role credentials when the assume-role fails — so without
// this grant the fallback path AccessDenies and the task dies at turn 0.
const bedrockGeoRegion = resolveBedrockGeoRegion(this.node);
const bedrockResources: string[] = [];
for (const modelId of resolveBedrockModelIds(this.node)) {
bedrockResources.push(
Expand All @@ -1212,7 +1213,7 @@ export class LambdaMicrovmCompute extends Construct {
stack.formatArn({
service: 'bedrock',
resource: 'inference-profile',
resourceName: `us.${modelId}`,
resourceName: inferenceProfileId(bedrockGeoRegion, modelId),
arnFormat: ArnFormat.SLASH_RESOURCE_NAME,
}),
);
Expand Down
35 changes: 35 additions & 0 deletions cdk/test/constructs/lambda-microvm-compute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,41 @@ function build(options: BuildOptions = {}): Built {
return { stack, construct, template: Template.fromStack(stack) };
}

describe.each(['global', 'eu', 'us', undefined])('LambdaMicrovmCompute — bedrockGeoRegion=%s', (geo) => {
let template: Template;

beforeAll(() => {
template = build({ context: geo === undefined ? {} : { bedrockGeoRegion: geo } }).template;
});

test('execution role grants only the selected inference profiles and bare foundation models', () => {
const policies = Object.entries(template.findResources('AWS::IAM::Policy'))
.filter(([id]) => id.includes('LambdaMicrovmComputeExecutionRole'));
const statements = policies.flatMap(([, policy]) => policy.Properties.PolicyDocument.Statement)
.filter((statement) => {
const actions = Array.isArray(statement.Action) ? statement.Action : [statement.Action];
return actions.includes('bedrock:InvokeModel');
});
expect(statements).toHaveLength(1);
expect(statements[0].Action).toEqual([
'bedrock:InvokeModel',
'bedrock:InvokeModelWithResponseStream',
]);
const expectedGeo = geo ?? 'us';
const resources = statements[0].Resource;
expect(resources).toHaveLength(DEFAULT_BEDROCK_MODEL_IDS.length * 2);
expect(resources).not.toContain('*');
const rendered = JSON.stringify(resources);
for (const modelId of DEFAULT_BEDROCK_MODEL_IDS) {
expect(rendered).toContain(`:bedrock:*::foundation-model/${modelId}`);
expect(rendered).toContain(`:bedrock:us-east-1:123456789012:inference-profile/${expectedGeo}.${modelId}`);
}
if (expectedGeo !== 'us') {
expect(rendered).not.toContain('inference-profile/us.');
}
});
});

describe('LambdaMicrovmCompute — image provisioned from a managed base image', () => {
let built: Built;
let template: Template;
Expand Down
Loading