@intentius/chant-lexicon-aws 0.45.0 → 0.49.0

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 (147) hide show
  1. package/dist/api/read-client.d.ts +10 -2
  2. package/dist/api/read-client.d.ts.map +1 -1
  3. package/dist/components/cloud-executor.d.ts.map +1 -1
  4. package/dist/composites/agentcore-agent.d.ts +26 -1
  5. package/dist/composites/agentcore-agent.d.ts.map +1 -1
  6. package/dist/composites/bucket-deployment.d.ts +82 -0
  7. package/dist/composites/bucket-deployment.d.ts.map +1 -0
  8. package/dist/composites/dynamodb-table.d.ts +45 -0
  9. package/dist/composites/dynamodb-table.d.ts.map +1 -0
  10. package/dist/composites/ec2-instance-bundle.d.ts +61 -0
  11. package/dist/composites/ec2-instance-bundle.d.ts.map +1 -0
  12. package/dist/composites/ecr-repository.d.ts +49 -0
  13. package/dist/composites/ecr-repository.d.ts.map +1 -0
  14. package/dist/composites/fargate-service.d.ts +16 -0
  15. package/dist/composites/fargate-service.d.ts.map +1 -1
  16. package/dist/composites/index.d.ts +16 -2
  17. package/dist/composites/index.d.ts.map +1 -1
  18. package/dist/composites/lambda-function.d.ts +4 -4
  19. package/dist/composites/lambda-function.d.ts.map +1 -1
  20. package/dist/composites/monitoring-stack.d.ts +99 -0
  21. package/dist/composites/monitoring-stack.d.ts.map +1 -0
  22. package/dist/composites/nlb-service.d.ts +48 -0
  23. package/dist/composites/nlb-service.d.ts.map +1 -0
  24. package/dist/composites/step-functions-workflow.d.ts +36 -0
  25. package/dist/composites/step-functions-workflow.d.ts.map +1 -0
  26. package/dist/deep-observe.d.ts +45 -0
  27. package/dist/deep-observe.d.ts.map +1 -1
  28. package/dist/deep-topology.d.ts +104 -0
  29. package/dist/deep-topology.d.ts.map +1 -0
  30. package/dist/effect-receipt-row.d.ts +92 -0
  31. package/dist/effect-receipt-row.d.ts.map +1 -0
  32. package/dist/index.d.ts +6 -2
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/integrity.json +9 -4
  35. package/dist/lint/audit-catalog.d.ts.map +1 -1
  36. package/dist/lint/post-synth/cf-refs.d.ts +24 -0
  37. package/dist/lint/post-synth/cf-refs.d.ts.map +1 -1
  38. package/dist/lint/post-synth/index.d.ts.map +1 -1
  39. package/dist/lint/post-synth/waw059.d.ts +36 -0
  40. package/dist/lint/post-synth/waw059.d.ts.map +1 -0
  41. package/dist/lint/post-synth/waw060.d.ts +16 -0
  42. package/dist/lint/post-synth/waw060.d.ts.map +1 -0
  43. package/dist/lint/post-synth/waw061.d.ts +19 -0
  44. package/dist/lint/post-synth/waw061.d.ts.map +1 -0
  45. package/dist/lint/post-synth/waw062.d.ts +19 -0
  46. package/dist/lint/post-synth/waw062.d.ts.map +1 -0
  47. package/dist/lint/post-synth/waw063.d.ts +35 -0
  48. package/dist/lint/post-synth/waw063.d.ts.map +1 -0
  49. package/dist/manifest.json +1 -1
  50. package/dist/okf/index.md +5 -0
  51. package/dist/okf/rules/WAW059.md +25 -0
  52. package/dist/okf/rules/WAW060.md +17 -0
  53. package/dist/okf/rules/WAW061.md +17 -0
  54. package/dist/okf/rules/WAW062.md +27 -0
  55. package/dist/okf/rules/WAW063.md +19 -0
  56. package/dist/okf/types/Action.md +2 -0
  57. package/dist/okf/types/AutoScalingGroup.md +1 -0
  58. package/dist/okf/types/Bucket.md +2 -0
  59. package/dist/okf/types/ECRRepository.md +1 -0
  60. package/dist/okf/types/Function.md +1 -0
  61. package/dist/okf/types/GlobalTable.md +4 -0
  62. package/dist/okf/types/IamPolicy.md +3 -0
  63. package/dist/okf/types/InstanceProfile.md +4 -0
  64. package/dist/okf/types/LaunchTemplate.md +1 -0
  65. package/dist/okf/types/LoadBalancer.md +1 -0
  66. package/dist/okf/types/ManagedPolicy.md +4 -0
  67. package/dist/okf/types/Map.md +2 -0
  68. package/dist/okf/types/Queue.md +1 -0
  69. package/dist/okf/types/Role.md +3 -0
  70. package/dist/okf/types/SecurityGroup.md +1 -0
  71. package/dist/okf/types/StateMachine.md +4 -0
  72. package/dist/okf/types/Subnet.md +4 -0
  73. package/dist/okf/types/Table.md +2 -0
  74. package/dist/okf/types/Type.md +5 -0
  75. package/dist/okf/types/Vpc.md +1 -0
  76. package/dist/op/activities/aws-apply.d.ts +48 -5
  77. package/dist/op/activities/aws-apply.d.ts.map +1 -1
  78. package/dist/op/activities/index.d.ts +11 -4
  79. package/dist/op/activities/index.d.ts.map +1 -1
  80. package/dist/ownership.d.ts +18 -0
  81. package/dist/ownership.d.ts.map +1 -1
  82. package/dist/plugin.d.ts.map +1 -1
  83. package/dist/receipt-store.d.ts +109 -0
  84. package/dist/receipt-store.d.ts.map +1 -0
  85. package/dist/rules/cf-refs.ts +62 -0
  86. package/dist/rules/waw059.ts +353 -0
  87. package/dist/rules/waw060.ts +91 -0
  88. package/dist/rules/waw061.ts +67 -0
  89. package/dist/rules/waw062.ts +0 -0
  90. package/dist/rules/waw063.ts +0 -0
  91. package/dist/serializer.d.ts.map +1 -1
  92. package/dist/skills/chant-aws-carve-terraform.md +26 -23
  93. package/dist/teardown.d.ts +85 -0
  94. package/dist/teardown.d.ts.map +1 -0
  95. package/package.json +3 -3
  96. package/src/api/read-client.test.ts +9 -0
  97. package/src/api/read-client.ts +15 -2
  98. package/src/components/cloud-executor.ts +10 -1
  99. package/src/composites/agentcore-agent.ts +35 -13
  100. package/src/composites/bucket-deployment.test.ts +84 -0
  101. package/src/composites/bucket-deployment.ts +153 -0
  102. package/src/composites/composites.test.ts +325 -0
  103. package/src/composites/dynamodb-table.test.ts +154 -0
  104. package/src/composites/dynamodb-table.ts +124 -0
  105. package/src/composites/ec2-instance-bundle.ts +134 -0
  106. package/src/composites/ecr-repository.test.ts +118 -0
  107. package/src/composites/ecr-repository.ts +117 -0
  108. package/src/composites/fargate-service.ts +19 -3
  109. package/src/composites/index.ts +21 -2
  110. package/src/composites/monitoring-stack.test.ts +130 -0
  111. package/src/composites/monitoring-stack.ts +164 -0
  112. package/src/composites/nlb-service.ts +117 -0
  113. package/src/composites/step-functions-workflow.ts +127 -0
  114. package/src/deep-observe.test.ts +201 -1
  115. package/src/deep-observe.ts +204 -0
  116. package/src/deep-topology.test.ts +392 -0
  117. package/src/deep-topology.ts +237 -0
  118. package/src/effect-receipt-row.test.ts +164 -0
  119. package/src/effect-receipt-row.ts +147 -0
  120. package/src/index.ts +28 -7
  121. package/src/lifecycle-integration.test.ts +4 -0
  122. package/src/lint/audit-catalog.ts +11 -0
  123. package/src/lint/post-synth/cf-refs.ts +62 -0
  124. package/src/lint/post-synth/index.ts +10 -0
  125. package/src/lint/post-synth/waw059.test.ts +309 -0
  126. package/src/lint/post-synth/waw059.ts +353 -0
  127. package/src/lint/post-synth/waw060.test.ts +131 -0
  128. package/src/lint/post-synth/waw060.ts +91 -0
  129. package/src/lint/post-synth/waw061.test.ts +99 -0
  130. package/src/lint/post-synth/waw061.ts +67 -0
  131. package/src/lint/post-synth/waw062.test.ts +109 -0
  132. package/src/lint/post-synth/waw062.ts +0 -0
  133. package/src/lint/post-synth/waw063.test.ts +220 -0
  134. package/src/lint/post-synth/waw063.ts +0 -0
  135. package/src/op/activities/aws-apply.test.ts +111 -0
  136. package/src/op/activities/aws-apply.ts +100 -6
  137. package/src/op/activities/index.ts +20 -3
  138. package/src/ownership.test.ts +24 -1
  139. package/src/ownership.ts +37 -0
  140. package/src/plugin.ts +59 -10
  141. package/src/receipt-store.test.ts +204 -0
  142. package/src/receipt-store.ts +315 -0
  143. package/src/serializer-ownership.test.ts +18 -0
  144. package/src/serializer.ts +114 -3
  145. package/src/skills/chant-aws-carve-terraform.md +26 -23
  146. package/src/teardown.test.ts +258 -0
  147. package/src/teardown.ts +276 -0
@@ -0,0 +1,67 @@
1
+ /**
2
+ * WAW061: Subnet CIDR Not Contained In VPC CIDR
3
+ *
4
+ * A subnet's CidrBlock must fall entirely inside its VPC's CidrBlock.
5
+ * Stock CDK does not check this — CloudFormation accepts the template and
6
+ * only fails the `AWS::EC2::Subnet` resource at deploy time
7
+ * ("The CIDR ... is invalid" / "does not fall within the CIDR range of the
8
+ * VPC"), after the rest of the stack may have already started rolling out.
9
+ * This is a cheap static graph check: follow the subnet's VpcId Ref to a
10
+ * declared VPC in the same template and do IPv4 CIDR-range math.
11
+ *
12
+ * Stays quiet whenever the graph can't prove it: intrinsic CIDR blocks,
13
+ * IPv6, a VpcId that isn't a simple Ref to a declared AWS::EC2::VPC, or a
14
+ * VPC whose own CidrBlock isn't a literal string.
15
+ */
16
+
17
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
18
+ import { parseCFTemplate, findResourceRefs, ipv4CidrContains } from "./cf-refs";
19
+
20
+ export function checkSubnetCidrContainment(ctx: PostSynthContext): PostSynthDiagnostic[] {
21
+ const diagnostics: PostSynthDiagnostic[] = [];
22
+
23
+ for (const [_lexicon, output] of ctx.outputs) {
24
+ const template = parseCFTemplate(output);
25
+ if (!template?.Resources) continue;
26
+
27
+ for (const [logicalId, resource] of Object.entries(template.Resources)) {
28
+ if (resource.Type !== "AWS::EC2::Subnet") continue;
29
+
30
+ const props = resource.Properties ?? {};
31
+ const subnetCidr = props.CidrBlock;
32
+ if (typeof subnetCidr !== "string") continue;
33
+
34
+ const vpcRefs = findResourceRefs(props.VpcId);
35
+ if (vpcRefs.size !== 1) continue;
36
+ const [vpcId] = vpcRefs;
37
+
38
+ const vpc = template.Resources[vpcId];
39
+ if (!vpc || vpc.Type !== "AWS::EC2::VPC") continue;
40
+
41
+ const vpcCidr = vpc.Properties?.CidrBlock;
42
+ if (typeof vpcCidr !== "string") continue;
43
+
44
+ const contained = ipv4CidrContains(vpcCidr, subnetCidr);
45
+ if (contained === false) {
46
+ diagnostics.push({
47
+ checkId: "WAW061",
48
+ severity: "error",
49
+ message: `Subnet "${logicalId}" CidrBlock ${subnetCidr} is not contained within VPC "${vpcId}"'s CidrBlock ${vpcCidr} — CloudFormation rejects this at deploy time`,
50
+ entity: logicalId,
51
+ lexicon: "aws",
52
+ });
53
+ }
54
+ }
55
+ }
56
+
57
+ return diagnostics;
58
+ }
59
+
60
+ export const waw061: PostSynthCheck = {
61
+ id: "WAW061",
62
+ description: "Subnet CidrBlock falls outside its VPC's CidrBlock — fails at deploy time",
63
+
64
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
65
+ return checkSubnetCidrContainment(ctx);
66
+ },
67
+ };
Binary file
Binary file
@@ -1 +1 @@
1
- {"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAsC,MAAM,6BAA6B,CAAC;AA0YlG;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,UAoE3B,CAAC"}
1
+ {"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAsC,MAAM,6BAA6B,CAAC;AAiflG;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,UA4E3B,CAAC"}
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  skill: chant-aws-carve-terraform
3
- description: Demo carving a resource out of Terraform into native chant — advise, emit, bridge, apply — fully offline
3
+ description: Demo carving a resource out of Terraform into native chant — advise, emit, audit, bridge, apply — fully offline
4
4
  user-invocable: true
5
5
  ---
6
6
 
@@ -15,7 +15,7 @@ graduation.
15
15
  ## When to use
16
16
 
17
17
  - Someone asks "how does chant move things off Terraform?"
18
- - You want to show the advise → emit → bridge → apply loop end to end.
18
+ - You want to show the advise → emit → audit → bridge → apply loop end to end.
19
19
  - You are evaluating whether a Terraform estate is worth carving.
20
20
 
21
21
  ## Preconditions
@@ -27,7 +27,7 @@ graduation.
27
27
  ## The fastest path: run the bundled demo
28
28
 
29
29
  The `examples/terraform-carve-out` example ships a runnable estate + state and a
30
- script that runs all four steps with commentary:
30
+ script that runs all five steps with commentary:
31
31
 
32
32
  ```bash
33
33
  cd examples/terraform-carve-out
@@ -54,28 +54,30 @@ From `examples/terraform-carve-out`, with `TF=./terraform`:
54
54
  --state ./terraform/terraform.tfstate --output ./carveout
55
55
  ```
56
56
  Show `./carveout/src/assets.ts` — a real `new Bucket({ BucketName, Tags })` with
57
- CloudFormation-style properties mapped from the Terraform state attributes.
58
- Emit also scaffolds `./carveout` into a buildable chant project
59
- (`chant.config.ts`, `package.json`), so `npm install && npm run build` works
60
- there as-is. Explain: a Terraform-managed resource is not in any
61
- CloudFormation stack, so the correct source of its live shape is the state
62
- file, not a cloud read.
63
-
64
- 3. **Lint the inherited resource — optional, offer it.**
57
+ CloudFormation-style properties mapped from the Terraform state attributes,
58
+ and the `aws_s3_bucket_versioning` sub-resource folded into
59
+ `VersioningConfiguration`. Emit also scaffolds `./carveout` into a buildable
60
+ chant project (`chant.config.ts`, `package.json`) and persists a carve
61
+ manifest (`*.carve.json`) bridge and apply read the target from it, so
62
+ `--select` is only needed once. Explain: a Terraform-managed resource is not
63
+ in any CloudFormation stack, so the correct source of its live shape is the
64
+ state file, not a cloud read.
65
+
66
+ 3. **Audit the inherited resource.**
65
67
  ```bash
66
- chant lint ./carveout/src --lexicon aws
68
+ chant build ./carveout/src --lexicon aws
67
69
  ```
68
- After emit, offer to lint the carved source. chant audits the resource you
69
- inherited from Terraform against the AWS lexicon's rules. Findings have a
70
- severity: `error` (must fix before `chant build` will emit e.g. an S3
71
- bucket with no public-access block) and `warning`/`info` (advisory — e.g.
72
- DynamoDB point-in-time recovery). This is a feature of carving: chant
73
- immediately tells the person what is wrong with what they adopted. Whether to
74
- fix an advisory finding is their call; errors block the build until resolved.
70
+ The first build fails, deliberately: the post-synth audit refuses the
71
+ adopted bucket because Terraform managed it without a public-access block or
72
+ a TLS-only policy. This is a feature of carving: chant immediately tells the
73
+ person what is wrong with what they adopted. Show the fix (add
74
+ `PublicAccessBlockConfiguration` and a companion `S3BucketPolicy` with an
75
+ `aws:SecureTransport = false` Deny the tutorial page has the exact code),
76
+ then re-run the build and show the valid CloudFormation template.
75
77
 
76
78
  4. **Bridge — patch the surviving Terraform.**
77
79
  ```bash
78
- chant carve bridge --from ./terraform --select aws_s3_bucket.assets --output ./carveout
80
+ chant carve bridge --from ./terraform --output ./carveout
79
81
  ```
80
82
  Show the generated `data "aws_s3_bucket" "assets"` and the rewired survivor.
81
83
  Emphasize it is dry-run: nothing in `./terraform` changed. `--apply-rewrites`
@@ -83,11 +85,12 @@ From `examples/terraform-carve-out`, with `TF=./terraform`:
83
85
 
84
86
  5. **Apply — graduation plan.**
85
87
  ```bash
86
- chant carve apply --from ./terraform --select aws_s3_bucket.assets --env prod --stack assets
88
+ chant carve apply --from ./terraform --output ./carveout --env prod --stack assets
87
89
  ```
88
90
  Show the ownership marker (`chant:managed-by/stack/env`) and the finalized
89
- runbook. Stress this makes no cloud call the apply is the person's own
90
- lifecycle; chant just plans it.
91
+ runbook. `--write-source` additionally stamps the marker tags into the
92
+ emitted source. Stress this makes no cloud call — the apply is the person's
93
+ own lifecycle; chant just plans it.
91
94
 
92
95
  ## Key points to land
93
96
 
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Env teardown for the aws lexicon (chant #1222) — both halves of the
3
+ * `teardownOwned` / `executeTeardown` capability pair, at STACK granularity.
4
+ *
5
+ * Per-resource selection is impossible here by construction: the thin read
6
+ * (`describeResources`) is sourced from `describe-stack-resources`, which
7
+ * returns no tags, so no resource-level marker can be read on the teardown
8
+ * path. The stack is aws's ownership boundary anyway — the applier deploys
9
+ * whole stacks and deletes ride CloudFormation — so teardown enumerates the
10
+ * environment's stacks and verifies the marker on each STACK's own tags
11
+ * (`DescribeStacks` → `Tags`), stamped there by the apply paths from the
12
+ * template's `Metadata["chant:ownership"]` block.
13
+ *
14
+ * Which stacks are the environment's: the project's declared `stacks[]` when
15
+ * it is a multi-stack project, else the single-stack convention — the explicit
16
+ * `stack` option, else the stack named after the environment (the same rule
17
+ * `describeResources` and `exportResources` apply, see chant #932).
18
+ *
19
+ * Verification is tag-reading, never name-trusting. A resolved stack whose
20
+ * tags carry exactly the requested identity (managed-by + stack + env) is a
21
+ * candidate of type `AWS::CloudFormation::Stack`. A stack carrying a DIFFERENT
22
+ * marker identity verifiably belongs to another project or env — out of scope,
23
+ * silently, the way another env's resources are. A stack carrying NO marker at
24
+ * all is unknowable — a legacy chant stack from before stack tagging, or a
25
+ * foreign stack that happens to hold the env's name — and is reported as a
26
+ * hole (`filtered` / unverified-ownership), never deleted. An absent stack is
27
+ * knowledge, not a hole: nothing to tear down.
28
+ *
29
+ * Execution re-reads each candidate stack's tags immediately before deleting
30
+ * (the enumeration is a moment old at best, and a delete is not undoable),
31
+ * then rides the existing Query-API delete — `awsDelete`, DeleteStack polled
32
+ * to DELETE_COMPLETE. An identity that no longer matches is `not-prunable:
33
+ * unverified-ownership`; an already-absent stack is `deleted` (teardown is
34
+ * idempotent).
35
+ */
36
+ import type { TeardownCandidate, TeardownEnumeration, TeardownExecution } from "@intentius/chant/lexicon";
37
+ import { type OwnershipMarker } from "@intentius/chant/ownership";
38
+ import { type AwsReadClientOptions } from "./api/read-client.js";
39
+ import { type AwsHttp } from "./op/activities/aws-apply.js";
40
+ /** The one candidate type this lexicon's teardown produces: whole stacks. */
41
+ export declare const STACK_TYPE = "AWS::CloudFormation::Stack";
42
+ export interface AwsTeardownOptions {
43
+ environment: string;
44
+ /** The identity to select on: this project's ownership stack + the env being torn down. */
45
+ marker: OwnershipMarker;
46
+ /** Explicit deployed stack name (single-stack override). */
47
+ stack?: string;
48
+ /** Region that stack is deployed in. */
49
+ region?: string;
50
+ /** A multi-stack project's declared stacks (chant.config `stacks[]`). */
51
+ stacks?: Array<{
52
+ name: string;
53
+ region?: string;
54
+ }>;
55
+ }
56
+ /** Injectable transports/timeouts, so tests double the Query API (no network). */
57
+ export interface AwsTeardownDeps {
58
+ /** Options for the read client's `DescribeStacks` calls (http injection, endpoint). */
59
+ read?: AwsReadClientOptions;
60
+ /** The applier transport `awsDelete` polls DeleteStack through. */
61
+ applyHttp?: AwsHttp;
62
+ /** DeleteStack settle timeout in ms (default `awsDelete`'s 300000). */
63
+ timeoutMs?: number;
64
+ /** DeleteStack poll interval in ms (default `awsDelete`'s 3000). */
65
+ intervalMs?: number;
66
+ }
67
+ /** The stacks an env teardown resolves to: `stacks[]`, else the single-stack convention. */
68
+ export declare function resolveTeardownStacks(options: AwsTeardownOptions): Array<{
69
+ name: string;
70
+ region?: string;
71
+ }>;
72
+ /**
73
+ * Enumerate the environment's marker-verified stacks — the aws half of
74
+ * `chant lifecycle teardown <env>` planning. Read-only.
75
+ */
76
+ export declare function teardownOwned(options: AwsTeardownOptions, deps?: AwsTeardownDeps): Promise<TeardownEnumeration>;
77
+ /**
78
+ * Delete the handed-over stacks: re-verify each stack's own tags, then
79
+ * DeleteStack via `awsDelete`, polled to DELETE_COMPLETE. One outcome per
80
+ * candidate, always.
81
+ */
82
+ export declare function executeTeardown(options: AwsTeardownOptions & {
83
+ candidates: TeardownCandidate[];
84
+ }, deps?: AwsTeardownDeps): Promise<TeardownExecution>;
85
+ //# sourceMappingURL=teardown.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"teardown.d.ts","sourceRoot":"","sources":["../src/teardown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,KAAK,EACV,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,EAGlB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAiB,KAAK,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAEjF,OAAO,EAKL,KAAK,oBAAoB,EAC1B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAa,KAAK,OAAO,EAAE,MAAM,2BAA2B,CAAC;AAEpE,6EAA6E;AAC7E,eAAO,MAAM,UAAU,+BAA+B,CAAC;AAEvD,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,2FAA2F;IAC3F,MAAM,EAAE,eAAe,CAAC;IACxB,4DAA4D;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACnD;AAED,kFAAkF;AAClF,MAAM,WAAW,eAAe;IAC9B,uFAAuF;IACvF,IAAI,CAAC,EAAE,oBAAoB,CAAC;IAC5B,mEAAmE;IACnE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,4FAA4F;AAC5F,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,kBAAkB,GAC1B,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAQ1C;AAmDD;;;GAGG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,kBAAkB,EAC3B,IAAI,GAAE,eAAoB,GACzB,OAAO,CAAC,mBAAmB,CAAC,CAuD9B;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,kBAAkB,GAAG;IAAE,UAAU,EAAE,iBAAiB,EAAE,CAAA;CAAE,EACjE,IAAI,GAAE,eAAoB,GACzB,OAAO,CAAC,iBAAiB,CAAC,CAW5B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-aws",
3
- "version": "0.45.0",
3
+ "version": "0.49.0",
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",
@@ -73,14 +73,14 @@
73
73
  },
74
74
  "dependencies": {
75
75
  "fflate": "^0.8.2",
76
- "js-yaml": "^4.1.0"
76
+ "js-yaml": "^4.3.1"
77
77
  },
78
78
  "devDependencies": {
79
79
  "@intentius/chant": "*",
80
80
  "typescript": "^5.9.3"
81
81
  },
82
82
  "peerDependencies": {
83
- "@intentius/chant": "^0.45.0",
83
+ "@intentius/chant": "^0.49.0",
84
84
  "typescript": "^5.9.3"
85
85
  }
86
86
  }
@@ -198,6 +198,15 @@ describe("Cloud Control", () => {
198
198
  expect(await listResources("AWS::S3::Bucket", { http })).toEqual([]);
199
199
  });
200
200
 
201
+ test("listResources scopes a child listing with ResourceModel, JSON-encoded as the API takes it", async () => {
202
+ const { http, calls } = recording(() => respond(JSON.stringify({ ResourceDescriptions: [] })));
203
+ await listResources("AWS::IAM::RolePolicy", { http }, { RoleName: "app-role" });
204
+ expect(JSON.parse(calls[0].body)).toEqual({
205
+ TypeName: "AWS::IAM::RolePolicy",
206
+ ResourceModel: JSON.stringify({ RoleName: "app-role" }),
207
+ });
208
+ });
209
+
201
210
  test("parseResourceDescription refuses anything that is not a model object", () => {
202
211
  expect(parseResourceDescription(null)).toBeNull();
203
212
  expect(parseResourceDescription({ Identifier: "a" })).toBeNull();
@@ -434,17 +434,30 @@ export async function getResource(
434
434
  return parseResourceDescription(body.ResourceDescription);
435
435
  }
436
436
 
437
- /** `ListResources` — every live resource of one type, paginated to exhaustion. */
437
+ /**
438
+ * `ListResources` — every live resource of one type, paginated to exhaustion.
439
+ *
440
+ * `resourceModel` is Cloud Control's `ResourceModel`: the scope a child-typed
441
+ * listing requires (`{ RoleName }` for `AWS::IAM::RolePolicy`,
442
+ * `{ TopicArn }` for `AWS::SNS::Subscription`). The API takes it as a JSON
443
+ * *string*, the same double encoding `GetResource` answers with; this takes
444
+ * the object and encodes it so no caller repeats that detail.
445
+ */
438
446
  export async function listResources(
439
447
  typeName: string,
440
448
  options: AwsReadClientOptions = {},
449
+ resourceModel?: Record<string, unknown>,
441
450
  ): Promise<CloudControlDescription[]> {
442
451
  const out: CloudControlDescription[] = [];
443
452
  let nextToken: string | undefined;
444
453
  do {
445
454
  const body = await cloudControl(
446
455
  "ListResources",
447
- { TypeName: typeName, ...(nextToken ? { NextToken: nextToken } : {}) },
456
+ {
457
+ TypeName: typeName,
458
+ ...(resourceModel ? { ResourceModel: JSON.stringify(resourceModel) } : {}),
459
+ ...(nextToken ? { NextToken: nextToken } : {}),
460
+ },
448
461
  options,
449
462
  );
450
463
  const descriptions = Array.isArray(body.ResourceDescriptions) ? body.ResourceDescriptions : [];
@@ -24,6 +24,7 @@ import { tmpdir } from "node:os";
24
24
  import { join } from "node:path";
25
25
  import { realDocker, type DockerClient } from "@intentius/chant/components/verbs/cloud-executor";
26
26
  import { q } from "@intentius/chant/components/verbs/process-runner";
27
+ import { ownershipStackTagsForBody } from "../ownership.js";
27
28
 
28
29
  export type { DockerClient };
29
30
 
@@ -432,10 +433,18 @@ const realCloudFormation: CloudFormationClient = {
432
433
  body = readFileSync(args.templatePath, "utf8");
433
434
  } catch { /* unreadable template — create-change-set reports it */ }
434
435
  const capabilities = awsDeployCapabilitiesForBody(body).join(" ");
436
+ // The template's ownership marker becomes the STACK's own tags (#1222) —
437
+ // the identity stack-level teardown verifies before a DeleteStack. Same
438
+ // source as the applier's `Tags.member.N`: `Metadata["chant:ownership"]`.
439
+ const stackTags = Object.entries(ownershipStackTagsForBody(body))
440
+ .sort(([a], [b]) => a.localeCompare(b))
441
+ .map(([k, v]) => q(`Key=${k},Value=${v}`))
442
+ .join(" ");
443
+ const tagsFlag = stackTags ? ` --tags ${stackTags}` : "";
435
444
  await run(
436
445
  `aws cloudformation create-change-set --stack-name ${q(args.stackName)} --change-set-name ${q(changeSetName)} ` +
437
446
  `--change-set-type ${changeSetType} ` +
438
- `--template-body file://${args.templatePath} --capabilities ${capabilities}${paramFlag}`,
447
+ `--template-body file://${args.templatePath} --capabilities ${capabilities}${paramFlag}${tagsFlag}`,
439
448
  );
440
449
  await run(
441
450
  `aws cloudformation wait change-set-create-complete --stack-name ${q(args.stackName)} --change-set-name ${q(changeSetName)}`,
@@ -45,19 +45,36 @@ export function agentCoreDefaultEndpointArn(runtime: InstanceType<typeof Runtime
45
45
  }
46
46
 
47
47
  /**
48
- * AgentCore's `Runtime`/`RuntimeEndpoint`/`Memory` `Name`/`AgentRuntimeName`
49
- * fields are all bound to the CFN registry pattern
50
- * `^[a-zA-Z][a-zA-Z0-9_]{0,47}$` (spot-checked against the CloudFormation
51
- * schema for #882 — no hyphens, unlike chant's usual kebab-case component
52
- * names). `Gateway`/`GatewayTarget`/`WorkloadIdentity` names accept hyphens,
53
- * so only the Runtime family needs sanitizing. Non-identifier characters
54
- * become `_`, and a leading digit/underscore gets an `A` prefix so the result
55
- * always starts with a letter.
48
+ * Bedrock AgentCore's real limits, verified against the CloudFormation
49
+ * registry schema for `AWS::BedrockAgentCore::{Runtime,RuntimeEndpoint,Memory}`
50
+ * (#882).
51
+ *
52
+ * Exported for the same reason as `MICROVM_LIMITS` (#1374, #1420): a
53
+ * consumer driving AgentCore through another control plane needs the same
54
+ * numbers, and copying them is how two sources of truth start.
55
+ */
56
+ export const AGENTCORE_LIMITS = {
57
+ /**
58
+ * `Runtime`/`RuntimeEndpoint`/`Memory` `Name`/`AgentRuntimeName` fields
59
+ * are bound to this pattern — no hyphens, unlike chant's usual kebab-case
60
+ * component names. `Gateway`/`GatewayTarget`/`WorkloadIdentity` names
61
+ * accept hyphens, so only the Runtime family needs sanitizing.
62
+ */
63
+ runtimeNamePattern: /^[a-zA-Z][a-zA-Z0-9_]{0,47}$/,
64
+ maxRuntimeNameLength: 48,
65
+ /** `Memory` event retention bounds, in days. */
66
+ memoryEventExpiryDays: { min: 3, max: 365, default: 30 },
67
+ } as const;
68
+
69
+ /**
70
+ * Non-identifier characters become `_`, and a leading digit/underscore gets
71
+ * an `A` prefix so the result always starts with a letter — then truncated
72
+ * to {@link AGENTCORE_LIMITS}'s `maxRuntimeNameLength`.
56
73
  */
57
74
  function toRuntimeIdentifier(name: string): string {
58
75
  const cleaned = name.replace(/[^a-zA-Z0-9_]/g, "_");
59
76
  const identifier = /^[a-zA-Z]/.test(cleaned) ? cleaned : `A${cleaned}`;
60
- return identifier.slice(0, 48);
77
+ return identifier.slice(0, AGENTCORE_LIMITS.maxRuntimeNameLength);
61
78
  }
62
79
 
63
80
  /**
@@ -117,7 +134,7 @@ export interface AgentCoreAgentProps {
117
134
  * managed one and fails on a real apply (#978, see the composite doc).
118
135
  */
119
136
  endpointName?: string;
120
- /** Memory event retention, in days. CFN bounds: 3-365. Default: 30. */
137
+ /** Memory event retention, in days. Bounds: {@link AGENTCORE_LIMITS.memoryEventExpiryDays}. */
121
138
  memoryEventExpiryDays?: number;
122
139
  /** Gateway authorizer. Mirrors the generated `BedrockAgentCoreGateway_AuthorizerType` CFN enum. Default: "AWS_IAM". */
123
140
  gatewayAuthorizerType?: "CUSTOM_JWT" | "AWS_IAM" | "NONE" | "AUTHENTICATE_ONLY";
@@ -194,9 +211,14 @@ export const AgentCoreAgent = Composite<AgentCoreAgentProps, AgentCoreAgentResul
194
211
  if (networkMode === "VPC" && (props.vpcSubnetIds === undefined || props.vpcSecurityGroupIds === undefined)) {
195
212
  throw new Error("AgentCoreAgent requires vpcSubnetIds and vpcSecurityGroupIds when networkMode is \"VPC\"");
196
213
  }
197
- const memoryEventExpiryDays = props.memoryEventExpiryDays ?? 30;
198
- if (memoryEventExpiryDays < 3 || memoryEventExpiryDays > 365) {
199
- throw new Error("AgentCoreAgent memoryEventExpiryDays must be between 3 and 365");
214
+ const memoryEventExpiryDays = props.memoryEventExpiryDays ?? AGENTCORE_LIMITS.memoryEventExpiryDays.default;
215
+ if (
216
+ memoryEventExpiryDays < AGENTCORE_LIMITS.memoryEventExpiryDays.min ||
217
+ memoryEventExpiryDays > AGENTCORE_LIMITS.memoryEventExpiryDays.max
218
+ ) {
219
+ throw new Error(
220
+ `AgentCoreAgent memoryEventExpiryDays must be between ${AGENTCORE_LIMITS.memoryEventExpiryDays.min} and ${AGENTCORE_LIMITS.memoryEventExpiryDays.max}`,
221
+ );
200
222
  }
201
223
 
202
224
  const runtimeName = toRuntimeIdentifier(props.name);
@@ -0,0 +1,84 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { BucketDeployment } from "./bucket-deployment";
3
+
4
+ describe("BucketDeployment", () => {
5
+ test("private-by-default: encrypted, public access fully blocked, retained on delete, no policy member", () => {
6
+ const c = BucketDeployment({});
7
+ expect(c.bucket).toBeDefined();
8
+ expect((c as unknown as Record<string, unknown>).bucketPolicy).toBeUndefined();
9
+
10
+ const props = (c.bucket as any).props;
11
+ expect(props.BucketEncryption.props.ServerSideEncryptionConfiguration).toHaveLength(1);
12
+ expect(props.PublicAccessBlockConfiguration.props).toEqual({
13
+ BlockPublicAcls: true,
14
+ BlockPublicPolicy: true,
15
+ IgnorePublicAcls: true,
16
+ RestrictPublicBuckets: true,
17
+ });
18
+ expect(props.VersioningConfiguration).toBeUndefined();
19
+ expect(props.WebsiteConfiguration).toBeUndefined();
20
+
21
+ const attrs = (c.bucket as any).attributes;
22
+ expect(attrs.DeletionPolicy).toBe("Retain");
23
+ });
24
+
25
+ test("bucketName threads through, versioning enables Status: Enabled", () => {
26
+ const c = BucketDeployment({ bucketName: "my-content-bucket", versioned: true });
27
+ const props = (c.bucket as any).props;
28
+ expect(props.BucketName).toBe("my-content-bucket");
29
+ expect(props.VersioningConfiguration.props).toEqual({ Status: "Enabled" });
30
+ });
31
+
32
+ test("removalPolicy: destroy maps to DeletionPolicy: Delete", () => {
33
+ const c = BucketDeployment({ removalPolicy: "destroy" });
34
+ expect((c.bucket as any).attributes.DeletionPolicy).toBe("Delete");
35
+ });
36
+
37
+ test("website shape: opens the public-access block, sets WebsiteConfiguration, and attaches a scoped public-read bucketPolicy", () => {
38
+ const c = BucketDeployment({
39
+ website: { indexDocument: "index.html", errorDocument: "error.html" },
40
+ }) as unknown as { bucket: any; bucketPolicy: any };
41
+
42
+ const bucketProps = c.bucket.props;
43
+ expect(bucketProps.PublicAccessBlockConfiguration.props).toEqual({
44
+ BlockPublicAcls: false,
45
+ BlockPublicPolicy: false,
46
+ IgnorePublicAcls: false,
47
+ RestrictPublicBuckets: false,
48
+ });
49
+ expect(bucketProps.WebsiteConfiguration.props).toEqual({
50
+ IndexDocument: "index.html",
51
+ ErrorDocument: "error.html",
52
+ });
53
+
54
+ expect(c.bucketPolicy).toBeDefined();
55
+ expect(c.bucketPolicy.props.Bucket).toBe(c.bucket); // bucket.Ref is the resource instance itself
56
+ const statement = c.bucketPolicy.props.PolicyDocument.Statement[0];
57
+ expect(statement).toEqual({ Effect: "Allow", Principal: "*", Action: "s3:GetObject", Resource: expect.anything() });
58
+ });
59
+
60
+ test("website without errorDocument omits it rather than writing undefined", () => {
61
+ const c = BucketDeployment({ website: { indexDocument: "index.html" } });
62
+ const websiteProps = (c.bucket as any).props.WebsiteConfiguration.props;
63
+ expect(websiteProps).toEqual({ IndexDocument: "index.html" });
64
+ expect("ErrorDocument" in websiteProps).toBe(false);
65
+ });
66
+
67
+ test("tags pass straight through to the bucket", () => {
68
+ const c = BucketDeployment({ tags: [{ Key: "env", Value: "prod" }] });
69
+ expect((c.bucket as any).props.Tags).toEqual([{ Key: "env", Value: "prod" }]);
70
+ });
71
+
72
+ test("defaults escape hatch reaches the bucket (e.g. object-lock)", () => {
73
+ const c = BucketDeployment({ defaults: { bucket: { ObjectLockEnabled: true } } });
74
+ expect((c.bucket as any).props.ObjectLockEnabled).toBe(true);
75
+ });
76
+
77
+ test("defaults escape hatch reaches the bucket policy", () => {
78
+ const c = BucketDeployment({
79
+ website: { indexDocument: "index.html" },
80
+ defaults: { bucketPolicy: { Bucket: "explicit-bucket-name" } },
81
+ }) as unknown as { bucketPolicy: any };
82
+ expect(c.bucketPolicy.props.Bucket).toBe("explicit-bucket-name");
83
+ });
84
+ });