@intentius/chant-lexicon-aws 0.18.11 → 0.18.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/dist/composites/agentcore-agent.d.ts +74 -0
  2. package/dist/composites/agentcore-agent.d.ts.map +1 -0
  3. package/dist/composites/agentcore-trust-policy.d.ts +11 -0
  4. package/dist/composites/agentcore-trust-policy.d.ts.map +1 -0
  5. package/dist/composites/index.d.ts +4 -0
  6. package/dist/composites/index.d.ts.map +1 -1
  7. package/dist/composites/microvm-app.d.ts +116 -0
  8. package/dist/composites/microvm-app.d.ts.map +1 -0
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/integrity.json +22 -4
  12. package/dist/lint/audit-catalog.d.ts.map +1 -1
  13. package/dist/lint/post-synth/cf-refs.d.ts +14 -0
  14. package/dist/lint/post-synth/cf-refs.d.ts.map +1 -1
  15. package/dist/lint/post-synth/index.d.ts.map +1 -1
  16. package/dist/lint/post-synth/waw038.d.ts +12 -0
  17. package/dist/lint/post-synth/waw038.d.ts.map +1 -0
  18. package/dist/lint/post-synth/waw039.d.ts +11 -0
  19. package/dist/lint/post-synth/waw039.d.ts.map +1 -0
  20. package/dist/lint/post-synth/waw040.d.ts +13 -0
  21. package/dist/lint/post-synth/waw040.d.ts.map +1 -0
  22. package/dist/lint/post-synth/waw041.d.ts +10 -0
  23. package/dist/lint/post-synth/waw041.d.ts.map +1 -0
  24. package/dist/lint/post-synth/waw042.d.ts +12 -0
  25. package/dist/lint/post-synth/waw042.d.ts.map +1 -0
  26. package/dist/lint/post-synth/waw043.d.ts +11 -0
  27. package/dist/lint/post-synth/waw043.d.ts.map +1 -0
  28. package/dist/lint/post-synth/waw044.d.ts +18 -0
  29. package/dist/lint/post-synth/waw044.d.ts.map +1 -0
  30. package/dist/lint/post-synth/waw045.d.ts +11 -0
  31. package/dist/lint/post-synth/waw045.d.ts.map +1 -0
  32. package/dist/lint/post-synth/waw046.d.ts +12 -0
  33. package/dist/lint/post-synth/waw046.d.ts.map +1 -0
  34. package/dist/lint/post-synth/waw047.d.ts +11 -0
  35. package/dist/lint/post-synth/waw047.d.ts.map +1 -0
  36. package/dist/lint/post-synth/waw048.d.ts +11 -0
  37. package/dist/lint/post-synth/waw048.d.ts.map +1 -0
  38. package/dist/lint/post-synth/waw049.d.ts +21 -0
  39. package/dist/lint/post-synth/waw049.d.ts.map +1 -0
  40. package/dist/lint/post-synth/waw050.d.ts +11 -0
  41. package/dist/lint/post-synth/waw050.d.ts.map +1 -0
  42. package/dist/lint/post-synth/waw051.d.ts +11 -0
  43. package/dist/lint/post-synth/waw051.d.ts.map +1 -0
  44. package/dist/lint/post-synth/waw052.d.ts +12 -0
  45. package/dist/lint/post-synth/waw052.d.ts.map +1 -0
  46. package/dist/lint/post-synth/waw053.d.ts +10 -0
  47. package/dist/lint/post-synth/waw053.d.ts.map +1 -0
  48. package/dist/lint/post-synth/waw054.d.ts +11 -0
  49. package/dist/lint/post-synth/waw054.d.ts.map +1 -0
  50. package/dist/lint/post-synth/waw055.d.ts +11 -0
  51. package/dist/lint/post-synth/waw055.d.ts.map +1 -0
  52. package/dist/manifest.json +1 -1
  53. package/dist/op/activities/floci.d.ts +6 -0
  54. package/dist/op/activities/floci.d.ts.map +1 -1
  55. package/dist/plugin.d.ts.map +1 -1
  56. package/dist/rules/cf-refs.ts +23 -0
  57. package/dist/rules/waw038.ts +50 -0
  58. package/dist/rules/waw039.ts +51 -0
  59. package/dist/rules/waw040.ts +54 -0
  60. package/dist/rules/waw041.ts +48 -0
  61. package/dist/rules/waw042.ts +87 -0
  62. package/dist/rules/waw043.ts +57 -0
  63. package/dist/rules/waw044.ts +68 -0
  64. package/dist/rules/waw045.ts +58 -0
  65. package/dist/rules/waw046.ts +59 -0
  66. package/dist/rules/waw047.ts +50 -0
  67. package/dist/rules/waw048.ts +47 -0
  68. package/dist/rules/waw049.ts +100 -0
  69. package/dist/rules/waw050.ts +54 -0
  70. package/dist/rules/waw051.ts +49 -0
  71. package/dist/rules/waw052.ts +51 -0
  72. package/dist/rules/waw053.ts +53 -0
  73. package/dist/rules/waw054.ts +49 -0
  74. package/dist/rules/waw055.ts +49 -0
  75. package/dist/types/index.d.ts +11 -9
  76. package/package.json +3 -2
  77. package/src/composites/agentcore-agent.test.ts +188 -0
  78. package/src/composites/agentcore-agent.ts +195 -0
  79. package/src/composites/agentcore-trust-policy.ts +13 -0
  80. package/src/composites/index.ts +4 -0
  81. package/src/composites/microvm-app.test.ts +234 -0
  82. package/src/composites/microvm-app.ts +436 -0
  83. package/src/generated/index.d.ts +11 -9
  84. package/src/index.ts +2 -0
  85. package/src/lint/audit-catalog.ts +20 -0
  86. package/src/lint/post-synth/cf-refs.ts +23 -0
  87. package/src/lint/post-synth/index.ts +36 -0
  88. package/src/lint/post-synth/waw038.test.ts +78 -0
  89. package/src/lint/post-synth/waw038.ts +50 -0
  90. package/src/lint/post-synth/waw039.test.ts +72 -0
  91. package/src/lint/post-synth/waw039.ts +51 -0
  92. package/src/lint/post-synth/waw040.test.ts +71 -0
  93. package/src/lint/post-synth/waw040.ts +54 -0
  94. package/src/lint/post-synth/waw041.test.ts +72 -0
  95. package/src/lint/post-synth/waw041.ts +48 -0
  96. package/src/lint/post-synth/waw042.test.ts +138 -0
  97. package/src/lint/post-synth/waw042.ts +87 -0
  98. package/src/lint/post-synth/waw043.test.ts +84 -0
  99. package/src/lint/post-synth/waw043.ts +57 -0
  100. package/src/lint/post-synth/waw044.test.ts +112 -0
  101. package/src/lint/post-synth/waw044.ts +68 -0
  102. package/src/lint/post-synth/waw045.test.ts +101 -0
  103. package/src/lint/post-synth/waw045.ts +58 -0
  104. package/src/lint/post-synth/waw046.test.ts +112 -0
  105. package/src/lint/post-synth/waw046.ts +59 -0
  106. package/src/lint/post-synth/waw047.test.ts +64 -0
  107. package/src/lint/post-synth/waw047.ts +50 -0
  108. package/src/lint/post-synth/waw048.test.ts +77 -0
  109. package/src/lint/post-synth/waw048.ts +47 -0
  110. package/src/lint/post-synth/waw049.test.ts +139 -0
  111. package/src/lint/post-synth/waw049.ts +100 -0
  112. package/src/lint/post-synth/waw050.test.ts +75 -0
  113. package/src/lint/post-synth/waw050.ts +54 -0
  114. package/src/lint/post-synth/waw051.test.ts +78 -0
  115. package/src/lint/post-synth/waw051.ts +49 -0
  116. package/src/lint/post-synth/waw052.test.ts +54 -0
  117. package/src/lint/post-synth/waw052.ts +51 -0
  118. package/src/lint/post-synth/waw053.test.ts +59 -0
  119. package/src/lint/post-synth/waw053.ts +53 -0
  120. package/src/lint/post-synth/waw054.test.ts +49 -0
  121. package/src/lint/post-synth/waw054.ts +49 -0
  122. package/src/lint/post-synth/waw055.test.ts +43 -0
  123. package/src/lint/post-synth/waw055.ts +49 -0
  124. package/src/op/activities/floci.test.ts +26 -0
  125. package/src/op/activities/floci.ts +25 -9
  126. package/src/plugin.ts +3 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-aws",
3
- "version": "0.18.11",
3
+ "version": "0.18.13",
4
4
  "description": "AWS CloudFormation lexicon for chant — declarative IaC in TypeScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -80,6 +80,7 @@
80
80
  "typescript": "^5.9.3"
81
81
  },
82
82
  "peerDependencies": {
83
- "@intentius/chant": "^0.18.11"
83
+ "@intentius/chant": "^0.18.13",
84
+ "typescript": "^5.9.3"
84
85
  }
85
86
  }
@@ -0,0 +1,188 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { expandComposite } from "@intentius/chant";
3
+ import { AttrRef } from "@intentius/chant/attrref";
4
+ // resolveAttrRefs stamps logical names onto AttrRefs before serialization —
5
+ // normally the discover→resolve→serialize pipeline (`chant build`) does this;
6
+ // reaching into core directly here mirrors nested-stack-integration.test.ts's
7
+ // precedent for aws-lexicon integration-style tests.
8
+ import { resolveAttrRefs } from "../../../../packages/core/src/discovery/resolve";
9
+ import { AgentCoreAgent } from "./agentcore-agent";
10
+ import { awsSerializer } from "../serializer";
11
+
12
+ const baseProps = {
13
+ name: "support-agent",
14
+ containerUri: "123456789012.dkr.ecr.us-east-1.amazonaws.com/support-agent:latest",
15
+ };
16
+
17
+ describe("AgentCoreAgent", () => {
18
+ test("returns all 8 members", () => {
19
+ const instance = AgentCoreAgent(baseProps);
20
+ expect(Object.keys(instance.members)).toEqual([
21
+ "role", "gatewayRole", "runtime", "endpoint", "memory",
22
+ "workloadIdentity", "gateway", "gatewayTarget",
23
+ ]);
24
+ });
25
+
26
+ test("expandComposite produces correct logical names", () => {
27
+ const expanded = expandComposite("agent", AgentCoreAgent(baseProps));
28
+ expect(expanded.has("agentRole")).toBe(true);
29
+ expect(expanded.has("agentGatewayRole")).toBe(true);
30
+ expect(expanded.has("agentRuntime")).toBe(true);
31
+ expect(expanded.has("agentEndpoint")).toBe(true);
32
+ expect(expanded.has("agentMemory")).toBe(true);
33
+ expect(expanded.has("agentWorkloadIdentity")).toBe(true);
34
+ expect(expanded.has("agentGateway")).toBe(true);
35
+ expect(expanded.has("agentGatewayTarget")).toBe(true);
36
+ expect(expanded.size).toBe(8);
37
+ });
38
+
39
+ test("role and gatewayRole trust bedrock-agentcore.amazonaws.com", () => {
40
+ const instance = AgentCoreAgent(baseProps);
41
+ const roleProps = (instance.role as any).props;
42
+ const gatewayRoleProps = (instance.gatewayRole as any).props;
43
+ expect(roleProps.AssumeRolePolicyDocument.Statement[0].Principal.Service).toBe(
44
+ "bedrock-agentcore.amazonaws.com",
45
+ );
46
+ expect(gatewayRoleProps.AssumeRolePolicyDocument.Statement[0].Principal.Service).toBe(
47
+ "bedrock-agentcore.amazonaws.com",
48
+ );
49
+ });
50
+
51
+ test("kebab-case name is sanitized for Runtime/RuntimeEndpoint/Memory (no hyphens)", () => {
52
+ const instance = AgentCoreAgent(baseProps);
53
+ const runtimeProps = (instance.runtime as any).props;
54
+ const endpointProps = (instance.endpoint as any).props;
55
+ const memoryProps = (instance.memory as any).props;
56
+ expect(runtimeProps.AgentRuntimeName).toBe("support_agent");
57
+ expect(runtimeProps.AgentRuntimeName).toMatch(/^[a-zA-Z][a-zA-Z0-9_]{0,47}$/);
58
+ expect(endpointProps.Name).toBe("DEFAULT");
59
+ expect(memoryProps.Name).toBe("support_agentMemory");
60
+ expect(memoryProps.Name).toMatch(/^[a-zA-Z][a-zA-Z0-9_]{0,47}$/);
61
+ });
62
+
63
+ test("Gateway/GatewayTarget/WorkloadIdentity keep the raw kebab-case name (hyphens allowed)", () => {
64
+ const instance = AgentCoreAgent(baseProps);
65
+ const gatewayProps = (instance.gateway as any).props;
66
+ const targetProps = (instance.gatewayTarget as any).props;
67
+ const identityProps = (instance.workloadIdentity as any).props;
68
+ expect(gatewayProps.Name).toBe("support-agent-gateway");
69
+ expect(targetProps.Name).toBe("support-agent-target");
70
+ expect(identityProps.Name).toBe("support-agent-identity");
71
+ });
72
+
73
+ test("Runtime wires RoleArn, ContainerUri, and defaults to MCP/PUBLIC", () => {
74
+ const instance = AgentCoreAgent(baseProps);
75
+ const runtimeProps = (instance.runtime as any).props;
76
+ expect(runtimeProps.RoleArn).toBeInstanceOf(AttrRef);
77
+ expect(runtimeProps.ProtocolConfiguration).toBe("MCP");
78
+ const artifactProps = (runtimeProps.AgentRuntimeArtifact as any).props;
79
+ const containerProps = (artifactProps.ContainerConfiguration as any).props;
80
+ expect(containerProps.ContainerUri).toBe(baseProps.containerUri);
81
+ const networkProps = (runtimeProps.NetworkConfiguration as any).props;
82
+ expect(networkProps.NetworkMode).toBe("PUBLIC");
83
+ expect(networkProps.NetworkModeConfig).toBeUndefined();
84
+ });
85
+
86
+ test("VPC network mode wires NetworkModeConfig from vpcSubnetIds/vpcSecurityGroupIds", () => {
87
+ const instance = AgentCoreAgent({
88
+ ...baseProps,
89
+ networkMode: "VPC",
90
+ vpcSubnetIds: ["subnet-1", "subnet-2"],
91
+ vpcSecurityGroupIds: ["sg-1"],
92
+ });
93
+ const runtimeProps = (instance.runtime as any).props;
94
+ const networkProps = (runtimeProps.NetworkConfiguration as any).props;
95
+ expect(networkProps.NetworkMode).toBe("VPC");
96
+ const vpcConfigProps = (networkProps.NetworkModeConfig as any).props;
97
+ expect(vpcConfigProps.Subnets).toEqual(["subnet-1", "subnet-2"]);
98
+ expect(vpcConfigProps.SecurityGroups).toEqual(["sg-1"]);
99
+ });
100
+
101
+ test("throws when networkMode is VPC without subnets/security groups", () => {
102
+ expect(() => AgentCoreAgent({ ...baseProps, networkMode: "VPC" })).toThrow(
103
+ "AgentCoreAgent requires vpcSubnetIds and vpcSecurityGroupIds when networkMode is \"VPC\"",
104
+ );
105
+ });
106
+
107
+ test("throws when memoryEventExpiryDays is out of CFN bounds (3-365)", () => {
108
+ expect(() => AgentCoreAgent({ ...baseProps, memoryEventExpiryDays: 1 })).toThrow(
109
+ "AgentCoreAgent memoryEventExpiryDays must be between 3 and 365",
110
+ );
111
+ expect(() => AgentCoreAgent({ ...baseProps, memoryEventExpiryDays: 400 })).toThrow(
112
+ "AgentCoreAgent memoryEventExpiryDays must be between 3 and 365",
113
+ );
114
+ });
115
+
116
+ test("RuntimeEndpoint references runtime.AgentRuntimeId", () => {
117
+ const instance = AgentCoreAgent(baseProps);
118
+ const endpointProps = (instance.endpoint as any).props;
119
+ expect(endpointProps.AgentRuntimeId).toBeInstanceOf(AttrRef);
120
+ });
121
+
122
+ test("Memory defaults EventExpiryDuration and MemoryExecutionRoleArn", () => {
123
+ const instance = AgentCoreAgent(baseProps);
124
+ const memoryProps = (instance.memory as any).props;
125
+ expect(memoryProps.EventExpiryDuration).toBe(30);
126
+ expect(memoryProps.MemoryExecutionRoleArn).toBeInstanceOf(AttrRef);
127
+ });
128
+
129
+ test("GatewayTarget defaults to routing back at the agent's own Runtime", () => {
130
+ const instance = AgentCoreAgent(baseProps);
131
+ const targetProps = (instance.gatewayTarget as any).props;
132
+ expect(targetProps.GatewayIdentifier).toBeInstanceOf(AttrRef);
133
+ const runtimeArnRef = targetProps.TargetConfiguration.Http.AgentcoreRuntime.Arn;
134
+ expect(runtimeArnRef).toBeInstanceOf(AttrRef);
135
+ });
136
+
137
+ test("Gateway defaults to AWS_IAM authorizer, override is respected", () => {
138
+ const instance = AgentCoreAgent(baseProps);
139
+ const gatewayProps = (instance.gateway as any).props;
140
+ expect(gatewayProps.AuthorizerType).toBe("AWS_IAM");
141
+
142
+ const custom = AgentCoreAgent({ ...baseProps, gatewayAuthorizerType: "NONE" });
143
+ const customGatewayProps = (custom.gateway as any).props;
144
+ expect(customGatewayProps.AuthorizerType).toBe("NONE");
145
+ });
146
+
147
+ test("ManagedPolicyArns and Policies pass through to the runtime role", () => {
148
+ const arn = "arn:aws:iam::aws:policy/AmazonBedrockFullAccess";
149
+ const instance = AgentCoreAgent({ ...baseProps, ManagedPolicyArns: [arn] });
150
+ const roleProps = (instance.role as any).props;
151
+ expect(roleProps.ManagedPolicyArns).toContain(arn);
152
+ });
153
+
154
+ test("per-member defaults are applied (e.g. custom endpoint description)", () => {
155
+ const instance = AgentCoreAgent({
156
+ ...baseProps,
157
+ defaults: { endpoint: { Description: "prod alias" } },
158
+ });
159
+ const endpointProps = (instance.endpoint as any).props;
160
+ expect(endpointProps.Description).toBe("prod alias");
161
+ });
162
+
163
+ test("serializes to a valid CloudFormation template with the expected resource types", () => {
164
+ const expanded = expandComposite("agent", AgentCoreAgent(baseProps));
165
+ resolveAttrRefs(expanded);
166
+ const output = awsSerializer.serialize(expanded);
167
+ const template = JSON.parse(output);
168
+
169
+ expect(template.AWSTemplateFormatVersion).toBe("2010-09-09");
170
+ expect(template.Resources.agentRole.Type).toBe("AWS::IAM::Role");
171
+ expect(template.Resources.agentGatewayRole.Type).toBe("AWS::IAM::Role");
172
+ expect(template.Resources.agentRuntime.Type).toBe("AWS::BedrockAgentCore::Runtime");
173
+ expect(template.Resources.agentEndpoint.Type).toBe("AWS::BedrockAgentCore::RuntimeEndpoint");
174
+ expect(template.Resources.agentMemory.Type).toBe("AWS::BedrockAgentCore::Memory");
175
+ expect(template.Resources.agentWorkloadIdentity.Type).toBe("AWS::BedrockAgentCore::WorkloadIdentity");
176
+ expect(template.Resources.agentGateway.Type).toBe("AWS::BedrockAgentCore::Gateway");
177
+ expect(template.Resources.agentGatewayTarget.Type).toBe("AWS::BedrockAgentCore::GatewayTarget");
178
+
179
+ // Cross-references resolved to CFN intrinsics, not left as live objects.
180
+ expect(template.Resources.agentRuntime.Properties.RoleArn).toEqual({ "Fn::GetAtt": ["agentRole", "Arn"] });
181
+ expect(template.Resources.agentEndpoint.Properties.AgentRuntimeId).toEqual({
182
+ "Fn::GetAtt": ["agentRuntime", "AgentRuntimeId"],
183
+ });
184
+ expect(template.Resources.agentGatewayTarget.Properties.TargetConfiguration).toEqual({
185
+ Http: { AgentcoreRuntime: { Arn: { "Fn::GetAtt": ["agentRuntime", "AgentRuntimeArn"] } } },
186
+ });
187
+ });
188
+ });
@@ -0,0 +1,195 @@
1
+ import { Composite, mergeDefaults } from "@intentius/chant";
2
+ import {
3
+ Role,
4
+ Role_Policy,
5
+ Runtime,
6
+ Runtime_AgentRuntimeArtifact,
7
+ Runtime_ContainerConfiguration,
8
+ Runtime_NetworkConfiguration,
9
+ Runtime_VpcConfig,
10
+ RuntimeEndpoint,
11
+ Memory,
12
+ BedrockAgentCoreGateway,
13
+ GatewayTarget,
14
+ WorkloadIdentity,
15
+ } from "../generated";
16
+ import { agentCoreTrustPolicy } from "./agentcore-trust-policy";
17
+
18
+ /**
19
+ * AgentCore's `Runtime`/`RuntimeEndpoint`/`Memory` `Name`/`AgentRuntimeName`
20
+ * fields are all bound to the CFN registry pattern
21
+ * `^[a-zA-Z][a-zA-Z0-9_]{0,47}$` (spot-checked against the CloudFormation
22
+ * schema for #882 — no hyphens, unlike chant's usual kebab-case component
23
+ * names). `Gateway`/`GatewayTarget`/`WorkloadIdentity` names accept hyphens,
24
+ * so only the Runtime family needs sanitizing. Non-identifier characters
25
+ * become `_`, and a leading digit/underscore gets an `A` prefix so the result
26
+ * always starts with a letter.
27
+ */
28
+ function toRuntimeIdentifier(name: string): string {
29
+ const cleaned = name.replace(/[^a-zA-Z0-9_]/g, "_");
30
+ const identifier = /^[a-zA-Z]/.test(cleaned) ? cleaned : `A${cleaned}`;
31
+ return identifier.slice(0, 48);
32
+ }
33
+
34
+ export interface AgentCoreAgentProps {
35
+ /**
36
+ * Base name for the agent's resources. `toRuntimeIdentifier(name)` derives
37
+ * the Runtime/RuntimeEndpoint/Memory names; Gateway/GatewayTarget/
38
+ * WorkloadIdentity use `name` as-is (hyphens are valid there).
39
+ */
40
+ name: string;
41
+ /** ECR image URI the Runtime runs, e.g. `"123456789012.dkr.ecr.us-east-1.amazonaws.com/agent:latest"`. */
42
+ containerUri: string;
43
+ /** Runtime network mode. Default: "PUBLIC". */
44
+ networkMode?: "PUBLIC" | "VPC";
45
+ /** Subnets for the Runtime's ENIs. Required when `networkMode` is "VPC". */
46
+ vpcSubnetIds?: string[];
47
+ /** Security groups for the Runtime's ENIs. Required when `networkMode` is "VPC". */
48
+ vpcSecurityGroupIds?: string[];
49
+ /** Protocol the Runtime serves. Mirrors the generated `Runtime_ProtocolConfiguration` CFN enum. Default: "MCP". */
50
+ protocolConfiguration?: "A2A" | "AGUI" | "HTTP" | "MCP";
51
+ /** Environment variables passed to the Runtime container. */
52
+ environmentVariables?: Record<string, string>;
53
+ /** RuntimeEndpoint name — the alias a version-promotion capability would repoint (deferred, see #882). Default: "DEFAULT". */
54
+ endpointName?: string;
55
+ /** Memory event retention, in days. CFN bounds: 3-365. Default: 30. */
56
+ memoryEventExpiryDays?: number;
57
+ /** Gateway authorizer. Mirrors the generated `BedrockAgentCoreGateway_AuthorizerType` CFN enum. Default: "AWS_IAM". */
58
+ gatewayAuthorizerType?: "CUSTOM_JWT" | "AWS_IAM" | "NONE" | "AUTHENTICATE_ONLY";
59
+ /** Allow-listed OAuth2 return URLs for the standalone `WorkloadIdentity` resource. */
60
+ allowedResourceOauth2ReturnUrls?: string[];
61
+ /** Extra managed policy ARNs merged onto the auto-created Runtime/Memory execution role. */
62
+ ManagedPolicyArns?: string[];
63
+ /** Inline policies attached to the auto-created Runtime/Memory execution role. */
64
+ Policies?: InstanceType<typeof Role_Policy>[];
65
+ defaults?: {
66
+ role?: Partial<ConstructorParameters<typeof Role>[0]>;
67
+ gatewayRole?: Partial<ConstructorParameters<typeof Role>[0]>;
68
+ runtime?: Partial<ConstructorParameters<typeof Runtime>[0]>;
69
+ endpoint?: Partial<ConstructorParameters<typeof RuntimeEndpoint>[0]>;
70
+ memory?: Partial<ConstructorParameters<typeof Memory>[0]>;
71
+ gateway?: Partial<ConstructorParameters<typeof BedrockAgentCoreGateway>[0]>;
72
+ gatewayTarget?: Partial<ConstructorParameters<typeof GatewayTarget>[0]>;
73
+ workloadIdentity?: Partial<ConstructorParameters<typeof WorkloadIdentity>[0]>;
74
+ };
75
+ }
76
+
77
+ export type AgentCoreAgentResult = {
78
+ role: InstanceType<typeof Role>;
79
+ gatewayRole: InstanceType<typeof Role>;
80
+ runtime: InstanceType<typeof Runtime>;
81
+ endpoint: InstanceType<typeof RuntimeEndpoint>;
82
+ memory: InstanceType<typeof Memory>;
83
+ workloadIdentity: InstanceType<typeof WorkloadIdentity>;
84
+ gateway: InstanceType<typeof BedrockAgentCoreGateway>;
85
+ gatewayTarget: InstanceType<typeof GatewayTarget>;
86
+ };
87
+
88
+ /**
89
+ * A Bedrock AgentCore agent as one CloudFormation-serializable bundle — the
90
+ * composite/base path from #882: `Runtime` + `RuntimeEndpoint` + `Memory` +
91
+ * `Gateway`/`GatewayTarget` + `WorkloadIdentity` + IAM, deployable with
92
+ * `cfn-deploy` + `wait-for-stack` and no bespoke verb.
93
+ *
94
+ * `WorkloadIdentityDetails` on both `Runtime` and `BedrockAgentCoreGateway`
95
+ * is CFN `readOnly` — AWS provisions a workload identity per resource
96
+ * automatically, and neither accepts one as an input. The `WorkloadIdentity`
97
+ * resource this composite creates is therefore standalone (not cross-wired
98
+ * to `runtime`/`gateway`), included because #882 calls for it — it exists
99
+ * for workflows (e.g. a future credential-provider capability) that need an
100
+ * explicit workload identity of their own.
101
+ *
102
+ * The `agentcore-deploy` version-promotion capability that would repoint
103
+ * `endpoint`'s `TargetVersion`/`LiveVersion` is deferred (GA-gated, #882) —
104
+ * this composite only wires the CloudFormation shape it would eventually
105
+ * apply against.
106
+ */
107
+ export const AgentCoreAgent = Composite<AgentCoreAgentProps, AgentCoreAgentResult>((props) => {
108
+ const { defaults } = props;
109
+ const networkMode = props.networkMode ?? "PUBLIC";
110
+ if (networkMode === "VPC" && (!props.vpcSubnetIds?.length || !props.vpcSecurityGroupIds?.length)) {
111
+ throw new Error("AgentCoreAgent requires vpcSubnetIds and vpcSecurityGroupIds when networkMode is \"VPC\"");
112
+ }
113
+ const memoryEventExpiryDays = props.memoryEventExpiryDays ?? 30;
114
+ if (memoryEventExpiryDays < 3 || memoryEventExpiryDays > 365) {
115
+ throw new Error("AgentCoreAgent memoryEventExpiryDays must be between 3 and 365");
116
+ }
117
+
118
+ const runtimeName = toRuntimeIdentifier(props.name);
119
+
120
+ // Runtime + Memory share one execution role — both are AgentCore-hosted
121
+ // and assume the same service principal (see agentcore-trust-policy.ts).
122
+ const role = new Role(mergeDefaults({
123
+ AssumeRolePolicyDocument: agentCoreTrustPolicy,
124
+ ManagedPolicyArns: props.ManagedPolicyArns,
125
+ Policies: props.Policies,
126
+ }, defaults?.role));
127
+
128
+ // Gateway assumes its own role to invoke targets (e.g. this agent's own
129
+ // Runtime endpoint) — kept separate from the Runtime/Memory role so its
130
+ // permissions don't have to grow with the agent's own execution needs.
131
+ const gatewayRole = new Role(mergeDefaults({
132
+ AssumeRolePolicyDocument: agentCoreTrustPolicy,
133
+ }, defaults?.gatewayRole));
134
+
135
+ const networkConfiguration = new Runtime_NetworkConfiguration({
136
+ NetworkMode: networkMode,
137
+ NetworkModeConfig: networkMode === "VPC"
138
+ ? new Runtime_VpcConfig({
139
+ SecurityGroups: props.vpcSecurityGroupIds,
140
+ Subnets: props.vpcSubnetIds,
141
+ })
142
+ : undefined,
143
+ });
144
+
145
+ const runtime = new Runtime(mergeDefaults({
146
+ AgentRuntimeName: runtimeName,
147
+ AgentRuntimeArtifact: new Runtime_AgentRuntimeArtifact({
148
+ ContainerConfiguration: new Runtime_ContainerConfiguration({
149
+ ContainerUri: props.containerUri,
150
+ }),
151
+ }),
152
+ RoleArn: role.Arn,
153
+ NetworkConfiguration: networkConfiguration,
154
+ ProtocolConfiguration: props.protocolConfiguration ?? "MCP",
155
+ EnvironmentVariables: props.environmentVariables,
156
+ }, defaults?.runtime));
157
+
158
+ const endpoint = new RuntimeEndpoint(mergeDefaults({
159
+ AgentRuntimeId: runtime.AgentRuntimeId,
160
+ Name: toRuntimeIdentifier(props.endpointName ?? "DEFAULT"),
161
+ }, defaults?.endpoint));
162
+
163
+ const memory = new Memory(mergeDefaults({
164
+ Name: `${runtimeName}Memory`.slice(0, 48),
165
+ EventExpiryDuration: memoryEventExpiryDays,
166
+ MemoryExecutionRoleArn: role.Arn,
167
+ }, defaults?.memory));
168
+
169
+ const workloadIdentity = new WorkloadIdentity(mergeDefaults({
170
+ Name: `${props.name}-identity`,
171
+ AllowedResourceOauth2ReturnUrls: props.allowedResourceOauth2ReturnUrls,
172
+ }, defaults?.workloadIdentity));
173
+
174
+ const gateway = new BedrockAgentCoreGateway(mergeDefaults({
175
+ Name: `${props.name}-gateway`,
176
+ AuthorizerType: props.gatewayAuthorizerType ?? "AWS_IAM",
177
+ RoleArn: gatewayRole.Arn,
178
+ }, defaults?.gateway));
179
+
180
+ // Default target: route the gateway's MCP tools straight back to this
181
+ // agent's own Runtime endpoint (the shape Gateway's HTTP/AgentcoreRuntime
182
+ // target configuration exists for). Override `defaults.gatewayTarget` to
183
+ // point at a Lambda/OpenAPI/other target instead.
184
+ const gatewayTarget = new GatewayTarget(mergeDefaults({
185
+ GatewayIdentifier: gateway.GatewayIdentifier,
186
+ Name: `${props.name}-target`,
187
+ TargetConfiguration: {
188
+ Http: {
189
+ AgentcoreRuntime: { Arn: runtime.AgentRuntimeArn },
190
+ },
191
+ },
192
+ }, defaults?.gatewayTarget));
193
+
194
+ return { role, gatewayRole, runtime, endpoint, memory, workloadIdentity, gateway, gatewayTarget };
195
+ }, "AgentCoreAgent");
@@ -0,0 +1,13 @@
1
+ // The service principal Bedrock AgentCore assumes to run a Runtime and to
2
+ // invoke Gateway targets. Same shape as ecs-trust-policy.ts/lambdaTrustPolicy
3
+ // in lambda-function.ts, one principal shared by both AgentCore-hosted roles.
4
+ export const agentCoreTrustPolicy = {
5
+ Version: "2012-10-17" as const,
6
+ Statement: [
7
+ {
8
+ Effect: "Allow" as const,
9
+ Principal: { Service: "bedrock-agentcore.amazonaws.com" },
10
+ Action: "sts:AssumeRole",
11
+ },
12
+ ],
13
+ };
@@ -32,3 +32,7 @@ export { Ec2InstanceRole } from "./ec2-instance-role";
32
32
  export type { Ec2InstanceRoleProps } from "./ec2-instance-role";
33
33
  export { MinimalVpc } from "./minimal-vpc";
34
34
  export type { MinimalVpcProps } from "./minimal-vpc";
35
+ export { MicrovmApp } from "./microvm-app";
36
+ export type { MicrovmAppProps, MicrovmAppResult, MicrovmAppBuildConnectorProps } from "./microvm-app";
37
+ export { AgentCoreAgent } from "./agentcore-agent";
38
+ export type { AgentCoreAgentProps, AgentCoreAgentResult } from "./agentcore-agent";
@@ -0,0 +1,234 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { expandComposite } from "@intentius/chant";
3
+ import { AttrRef } from "@intentius/chant/attrref";
4
+ import { resolveAttrRefs } from "@intentius/chant/discovery/resolve";
5
+ import { awsSerializer } from "../serializer";
6
+ import { MicrovmApp } from "./microvm-app";
7
+
8
+ const baseProps = {
9
+ name: "worker-image",
10
+ description: "Worker MicroVM image",
11
+ codeArtifactUri: "s3://microvm-artifacts/worker/context.zip",
12
+ codeArtifactObjectArn: "arn:aws:s3:::microvm-artifacts/worker/context.zip",
13
+ baseImage: "al2023-1",
14
+ baseImageVersion: "0",
15
+ };
16
+
17
+ describe("MicrovmApp", () => {
18
+ test("returns buildRole, executionRole, and image members (no connector)", () => {
19
+ const instance = MicrovmApp(baseProps);
20
+ expect(instance.buildRole).toBeDefined();
21
+ expect(instance.executionRole).toBeDefined();
22
+ expect(instance.image).toBeDefined();
23
+ expect(instance.connector).toBeUndefined();
24
+ expect(instance.connectorSecurityGroup).toBeUndefined();
25
+ expect(instance.connectorOperatorRole).toBeUndefined();
26
+ expect(Object.keys(instance.members)).toEqual(["buildRole", "executionRole", "image"]);
27
+ });
28
+
29
+ test("image.BuildRoleArn references buildRole.Arn via AttrRef", () => {
30
+ const instance = MicrovmApp(baseProps);
31
+ const imageProps = (instance.image as any).props;
32
+ expect(imageProps.BuildRoleArn).toBeInstanceOf(AttrRef);
33
+ });
34
+
35
+ test("defaults: memory 2048MiB, ARM_64, empty os capabilities, logging enabled", () => {
36
+ const instance = MicrovmApp(baseProps);
37
+ const imageProps = (instance.image as any).props;
38
+ expect(imageProps.Resources[0].props.MinimumMemoryInMiB).toBe(2048);
39
+ expect(imageProps.CpuConfigurations[0].props.Architecture).toBe("ARM_64");
40
+ expect(imageProps.AdditionalOsCapabilities).toEqual([]);
41
+ expect(imageProps.EgressNetworkConnectors).toEqual([]);
42
+ expect(imageProps.Logging.props.CloudWatch.props.LogGroup).toBe("/aws/lambda/microvms/worker-image");
43
+ });
44
+
45
+ test("disableLogging emits Logging.Disabled", () => {
46
+ const instance = MicrovmApp({ ...baseProps, disableLogging: true });
47
+ const imageProps = (instance.image as any).props;
48
+ expect(imageProps.Logging.props.Disabled).toBe(true);
49
+ expect(imageProps.Logging.props.CloudWatch).toBeUndefined();
50
+ });
51
+
52
+ test("environment variables become Key/Value pairs", () => {
53
+ const instance = MicrovmApp({ ...baseProps, environment: { MODE: "prod" } });
54
+ const imageProps = (instance.image as any).props;
55
+ expect(imageProps.EnvironmentVariables).toHaveLength(1);
56
+ expect(imageProps.EnvironmentVariables[0].props).toEqual({ Key: "MODE", Value: "prod" });
57
+ });
58
+
59
+ test("rejects a reserved AWS_REGION environment key", () => {
60
+ expect(() => MicrovmApp({ ...baseProps, environment: { AWS_REGION: "us-east-1" } })).toThrow(/reserved/);
61
+ });
62
+
63
+ test("rejects an invalid memoryMiB", () => {
64
+ expect(() => MicrovmApp({ ...baseProps, memoryMiB: 3000 as any })).toThrow(/memoryMiB/);
65
+ });
66
+
67
+ test("rejects an invalid name", () => {
68
+ expect(() => MicrovmApp({ ...baseProps, name: "bad name!" })).toThrow(/name must match/);
69
+ });
70
+
71
+ test("full ARN base image passes through; alias resolves via Sub", () => {
72
+ const aliasInstance = MicrovmApp(baseProps);
73
+ const aliasArn = (aliasInstance.image as any).props.BaseImageArn;
74
+ expect(aliasArn).not.toBe("al2023-1");
75
+ expect(typeof aliasArn).toBe("object"); // SubIntrinsic
76
+
77
+ const fullArn = "arn:aws:lambda:us-east-1:aws:microvm-image:al2023-1";
78
+ const arnInstance = MicrovmApp({ ...baseProps, baseImage: fullArn });
79
+ expect((arnInstance.image as any).props.BaseImageArn).toBe(fullArn);
80
+ });
81
+
82
+ describe("with buildConnector", () => {
83
+ const withConnector = {
84
+ ...baseProps,
85
+ buildConnector: { vpcId: "vpc-123", subnetIds: ["subnet-1", "subnet-2"] },
86
+ };
87
+
88
+ test("adds connectorSecurityGroup, connectorOperatorRole, and connector members", () => {
89
+ const instance = MicrovmApp(withConnector);
90
+ expect(instance.connectorSecurityGroup).toBeDefined();
91
+ expect(instance.connectorOperatorRole).toBeDefined();
92
+ expect(instance.connector).toBeDefined();
93
+ expect(Object.keys(instance.members)).toEqual([
94
+ "buildRole",
95
+ "executionRole",
96
+ "image",
97
+ "connectorSecurityGroup",
98
+ "connectorOperatorRole",
99
+ "connector",
100
+ ]);
101
+ });
102
+
103
+ test("connector security group is deny-all egress, no ingress", () => {
104
+ const instance = MicrovmApp(withConnector);
105
+ const sgProps = (instance.connectorSecurityGroup as any).props;
106
+ expect(sgProps.SecurityGroupIngress).toBeUndefined();
107
+ expect(sgProps.SecurityGroupEgress).toHaveLength(1);
108
+ expect(sgProps.SecurityGroupEgress[0].props).toEqual({
109
+ CidrIp: "255.255.255.255/32",
110
+ Description: "Disallow all traffic",
111
+ IpProtocol: "icmp",
112
+ FromPort: 252,
113
+ ToPort: 86,
114
+ });
115
+ });
116
+
117
+ test("image.EgressNetworkConnectors references connector.Arn", () => {
118
+ const instance = MicrovmApp(withConnector);
119
+ const imageProps = (instance.image as any).props;
120
+ expect(imageProps.EgressNetworkConnectors).toHaveLength(1);
121
+ expect(imageProps.EgressNetworkConnectors[0]).toBeInstanceOf(AttrRef);
122
+ });
123
+
124
+ test("rejects an out-of-range subnet count", () => {
125
+ expect(() =>
126
+ MicrovmApp({ ...baseProps, buildConnector: { vpcId: "vpc-1", subnetIds: [] } }),
127
+ ).toThrow(/subnets/);
128
+ });
129
+
130
+ test("additionalEgressConnectors plus a buildConnector over the max of 10 throws", () => {
131
+ const extra = Array.from({ length: 10 }, (_, i) => `arn:aws:lambda:us-east-1:123456789012:network-connector:nc-${i}`);
132
+ expect(() =>
133
+ MicrovmApp({ ...withConnector, additionalEgressConnectors: extra }),
134
+ ).toThrow(/egress connectors/);
135
+ });
136
+ });
137
+
138
+ test("expandComposite produces correct logical names (no connector)", () => {
139
+ const instance = MicrovmApp(baseProps);
140
+ const expanded = expandComposite("worker", instance);
141
+ expect(expanded.has("workerBuildRole")).toBe(true);
142
+ expect(expanded.has("workerExecutionRole")).toBe(true);
143
+ expect(expanded.has("workerImage")).toBe(true);
144
+ expect(expanded.size).toBe(3);
145
+ });
146
+
147
+ describe("CloudFormation serialization", () => {
148
+ test("serializes to a valid CloudFormation template (no connector)", () => {
149
+ const instance = MicrovmApp(baseProps);
150
+ const entities = expandComposite("worker", instance);
151
+ resolveAttrRefs(entities);
152
+ const output = awsSerializer.serialize(entities);
153
+ const template = JSON.parse(output);
154
+
155
+ expect(template.AWSTemplateFormatVersion).toBe("2010-09-09");
156
+
157
+ const image = template.Resources.workerImage;
158
+ expect(image.Type).toBe("AWS::Lambda::MicrovmImage");
159
+ expect(image.Properties.Name).toBe("worker-image");
160
+ expect(image.Properties.Description).toBe("Worker MicroVM image");
161
+ expect(image.Properties.CodeArtifact).toEqual({ Uri: "s3://microvm-artifacts/worker/context.zip" });
162
+ expect(image.Properties.CpuConfigurations).toEqual([{ Architecture: "ARM_64" }]);
163
+ expect(image.Properties.Resources).toEqual([{ MinimumMemoryInMiB: 2048 }]);
164
+ expect(image.Properties.AdditionalOsCapabilities).toEqual([]);
165
+ expect(image.Properties.EgressNetworkConnectors).toEqual([]);
166
+ expect(image.Properties.Hooks).toEqual({});
167
+ expect(image.Properties.BuildRoleArn).toEqual({ "Fn::GetAtt": ["workerBuildRole", "Arn"] });
168
+
169
+ const buildRole = template.Resources.workerBuildRole;
170
+ expect(buildRole.Type).toBe("AWS::IAM::Role");
171
+ expect(buildRole.Properties.AssumeRolePolicyDocument.Statement).toHaveLength(2);
172
+ expect(buildRole.Properties.Policies[0].PolicyDocument.Statement[0].Resource).toBe(
173
+ "arn:aws:s3:::microvm-artifacts/worker/context.zip",
174
+ );
175
+
176
+ const executionRole = template.Resources.workerExecutionRole;
177
+ expect(executionRole.Type).toBe("AWS::IAM::Role");
178
+ expect(executionRole.Properties.Policies[0].PolicyName).toBe("MicrovmRuntimeLogs");
179
+
180
+ // Required CFN properties for AWS::Lambda::MicrovmImage all present.
181
+ for (const key of [
182
+ "Name",
183
+ "BaseImageArn",
184
+ "BaseImageVersion",
185
+ "BuildRoleArn",
186
+ "Description",
187
+ "CodeArtifact",
188
+ "Logging",
189
+ "EgressNetworkConnectors",
190
+ "CpuConfigurations",
191
+ "Resources",
192
+ "AdditionalOsCapabilities",
193
+ "Hooks",
194
+ "EnvironmentVariables",
195
+ ]) {
196
+ expect(image.Properties).toHaveProperty(key);
197
+ }
198
+ });
199
+
200
+ test("serializes the network connector + its security group and operator role", () => {
201
+ const instance = MicrovmApp({
202
+ ...baseProps,
203
+ buildConnector: { vpcId: "vpc-123", subnetIds: ["subnet-1", "subnet-2"], networkProtocol: "DualStack" },
204
+ });
205
+ const entities = expandComposite("worker", instance);
206
+ resolveAttrRefs(entities);
207
+ const output = awsSerializer.serialize(entities);
208
+ const template = JSON.parse(output);
209
+
210
+ const connector = template.Resources.workerConnector;
211
+ expect(connector.Type).toBe("AWS::Lambda::NetworkConnector");
212
+ expect(connector.Properties.OperatorRole).toEqual({ "Fn::GetAtt": ["workerConnectorOperatorRole", "Arn"] });
213
+ expect(connector.Properties.Configuration.VpcEgressConfiguration).toEqual({
214
+ AssociatedComputeResourceTypes: ["MicroVm"],
215
+ SubnetIds: ["subnet-1", "subnet-2"],
216
+ SecurityGroupIds: [{ "Fn::GetAtt": ["workerConnectorSecurityGroup", "GroupId"] }],
217
+ NetworkProtocol: "DualStack",
218
+ });
219
+
220
+ const sg = template.Resources.workerConnectorSecurityGroup;
221
+ expect(sg.Type).toBe("AWS::EC2::SecurityGroup");
222
+ expect(sg.Properties.VpcId).toBe("vpc-123");
223
+
224
+ const operatorRole = template.Resources.workerConnectorOperatorRole;
225
+ expect(operatorRole.Type).toBe("AWS::IAM::Role");
226
+ expect(operatorRole.Properties.AssumeRolePolicyDocument.Statement).toHaveLength(1);
227
+
228
+ const image = template.Resources.workerImage;
229
+ expect(image.Properties.EgressNetworkConnectors).toEqual([
230
+ { "Fn::GetAtt": ["workerConnector", "Arn"] },
231
+ ]);
232
+ });
233
+ });
234
+ });