@intentius/chant-lexicon-cedar 0.44.8 → 0.44.10

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 (149) hide show
  1. package/README.md +106 -2
  2. package/dist/agentcore/embed.d.ts +189 -0
  3. package/dist/agentcore/embed.d.ts.map +1 -0
  4. package/dist/agentcore/enforcement.d.ts +76 -0
  5. package/dist/agentcore/enforcement.d.ts.map +1 -0
  6. package/dist/agentcore/scan.d.ts +46 -0
  7. package/dist/agentcore/scan.d.ts.map +1 -0
  8. package/dist/codegen/docs-dogwood.d.ts +21 -0
  9. package/dist/codegen/docs-dogwood.d.ts.map +1 -0
  10. package/dist/codegen/docs.d.ts.map +1 -1
  11. package/dist/codegen/package.d.ts.map +1 -1
  12. package/dist/config.d.ts +25 -0
  13. package/dist/config.d.ts.map +1 -1
  14. package/dist/dogwood/cli.d.ts +247 -0
  15. package/dist/dogwood/cli.d.ts.map +1 -0
  16. package/dist/dogwood/event-schema.d.ts +161 -0
  17. package/dist/dogwood/event-schema.d.ts.map +1 -0
  18. package/dist/dogwood/index.d.ts +39 -0
  19. package/dist/dogwood/index.d.ts.map +1 -0
  20. package/dist/dogwood/macros.d.ts +96 -0
  21. package/dist/dogwood/macros.d.ts.map +1 -0
  22. package/dist/dogwood/policy.d.ts +120 -0
  23. package/dist/dogwood/policy.d.ts.map +1 -0
  24. package/dist/dogwood/replay-activity.d.ts +196 -0
  25. package/dist/dogwood/replay-activity.d.ts.map +1 -0
  26. package/dist/dogwood/replay-op.d.ts +165 -0
  27. package/dist/dogwood/replay-op.d.ts.map +1 -0
  28. package/dist/dogwood/scan.d.ts +109 -0
  29. package/dist/dogwood/scan.d.ts.map +1 -0
  30. package/dist/dogwood/serialize.d.ts +66 -0
  31. package/dist/dogwood/serialize.d.ts.map +1 -0
  32. package/dist/dogwood/temporal.d.ts +259 -0
  33. package/dist/dogwood/temporal.d.ts.map +1 -0
  34. package/dist/dogwood/trace.d.ts +215 -0
  35. package/dist/dogwood/trace.d.ts.map +1 -0
  36. package/dist/dogwood/upstream.d.ts +41 -0
  37. package/dist/dogwood/upstream.d.ts.map +1 -0
  38. package/dist/dogwood/window.d.ts +73 -0
  39. package/dist/dogwood/window.d.ts.map +1 -0
  40. package/dist/index.d.ts +12 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/integrity.json +15 -3
  43. package/dist/lint/audit-catalog.d.ts.map +1 -1
  44. package/dist/lint/post-synth/dogwood-helpers.d.ts +63 -0
  45. package/dist/lint/post-synth/dogwood-helpers.d.ts.map +1 -0
  46. package/dist/lint/post-synth/dwdc010.d.ts +25 -0
  47. package/dist/lint/post-synth/dwdc010.d.ts.map +1 -0
  48. package/dist/lint/post-synth/dwdc011.d.ts +19 -0
  49. package/dist/lint/post-synth/dwdc011.d.ts.map +1 -0
  50. package/dist/lint/post-synth/dwdc012.d.ts +21 -0
  51. package/dist/lint/post-synth/dwdc012.d.ts.map +1 -0
  52. package/dist/lint/post-synth/dwdc013.d.ts +33 -0
  53. package/dist/lint/post-synth/dwdc013.d.ts.map +1 -0
  54. package/dist/lint/post-synth/dwde010.d.ts +32 -0
  55. package/dist/lint/post-synth/dwde010.d.ts.map +1 -0
  56. package/dist/lint/post-synth/dwde011.d.ts +33 -0
  57. package/dist/lint/post-synth/dwde011.d.ts.map +1 -0
  58. package/dist/lint/post-synth/dwds010.d.ts +24 -0
  59. package/dist/lint/post-synth/dwds010.d.ts.map +1 -0
  60. package/dist/lint/post-synth/index.d.ts.map +1 -1
  61. package/dist/manifest.json +1 -1
  62. package/dist/okf/index.md +7 -0
  63. package/dist/okf/rules/DWDC010.md +11 -0
  64. package/dist/okf/rules/DWDC011.md +11 -0
  65. package/dist/okf/rules/DWDC012.md +11 -0
  66. package/dist/okf/rules/DWDC013.md +15 -0
  67. package/dist/okf/rules/DWDE010.md +11 -0
  68. package/dist/okf/rules/DWDE011.md +11 -0
  69. package/dist/okf/rules/DWDS010.md +11 -0
  70. package/dist/okf/types/Policy.md +1 -0
  71. package/dist/op/activities/index.d.ts +18 -0
  72. package/dist/op/activities/index.d.ts.map +1 -0
  73. package/dist/plugin.d.ts.map +1 -1
  74. package/dist/policy-text.d.ts +53 -0
  75. package/dist/policy-text.d.ts.map +1 -0
  76. package/dist/rules/dogwood-helpers.ts +139 -0
  77. package/dist/rules/dwdc010.ts +62 -0
  78. package/dist/rules/dwdc011.ts +61 -0
  79. package/dist/rules/dwdc012.ts +46 -0
  80. package/dist/rules/dwdc013.ts +64 -0
  81. package/dist/rules/dwde010.ts +130 -0
  82. package/dist/rules/dwde011.ts +108 -0
  83. package/dist/rules/dwds010.ts +46 -0
  84. package/dist/serializer.d.ts +10 -18
  85. package/dist/serializer.d.ts.map +1 -1
  86. package/dist/skills/chant-cedar-authoring.md +180 -0
  87. package/dist/skills/chant-cedar-avp-embedding.md +125 -0
  88. package/dist/skills/chant-cedar-dogwood.md +327 -0
  89. package/dist/skills/chant-cedar-meta-policy.md +119 -0
  90. package/package.json +7 -2
  91. package/src/agentcore/embed.test.ts +254 -0
  92. package/src/agentcore/embed.ts +399 -0
  93. package/src/agentcore/enforcement.test.ts +43 -0
  94. package/src/agentcore/enforcement.ts +92 -0
  95. package/src/agentcore/scan.ts +119 -0
  96. package/src/codegen/docs-dogwood.ts +1119 -0
  97. package/src/codegen/docs.ts +66 -1
  98. package/src/codegen/package.ts +3 -2
  99. package/src/config.test.ts +12 -0
  100. package/src/config.ts +28 -0
  101. package/src/dogwood/cli.test.ts +513 -0
  102. package/src/dogwood/cli.ts +666 -0
  103. package/src/dogwood/event-schema.test.ts +218 -0
  104. package/src/dogwood/event-schema.ts +318 -0
  105. package/src/dogwood/index.ts +271 -0
  106. package/src/dogwood/macros.test.ts +104 -0
  107. package/src/dogwood/macros.ts +229 -0
  108. package/src/dogwood/policy.test.ts +94 -0
  109. package/src/dogwood/policy.ts +141 -0
  110. package/src/dogwood/replay-activity.test.ts +481 -0
  111. package/src/dogwood/replay-activity.ts +506 -0
  112. package/src/dogwood/replay-op.ts +242 -0
  113. package/src/dogwood/scan.ts +287 -0
  114. package/src/dogwood/serialize.test.ts +331 -0
  115. package/src/dogwood/serialize.ts +246 -0
  116. package/src/dogwood/temporal.test.ts +272 -0
  117. package/src/dogwood/temporal.ts +592 -0
  118. package/src/dogwood/testdata/custom-kinds.dwschema +17 -0
  119. package/src/dogwood/testdata/default-macros.dw +23 -0
  120. package/src/dogwood/testdata/lowered-read-after-login.json +13 -0
  121. package/src/dogwood/testdata/max-window-raised.dwschema +25 -0
  122. package/src/dogwood/testdata/pinned.dwschema +31 -0
  123. package/src/dogwood/testdata/read-after-login.cedarschema +20 -0
  124. package/src/dogwood/testdata/read-after-login.dw +17 -0
  125. package/src/dogwood/testdata/temporal-policies.dw +53 -0
  126. package/src/dogwood/trace.test.ts +231 -0
  127. package/src/dogwood/trace.ts +471 -0
  128. package/src/dogwood/upstream.ts +41 -0
  129. package/src/dogwood/window.ts +124 -0
  130. package/src/index.ts +76 -0
  131. package/src/lint/audit-catalog.ts +64 -0
  132. package/src/lint/post-synth/dogwood-helpers.ts +139 -0
  133. package/src/lint/post-synth/dwd-post-synth.test.ts +374 -0
  134. package/src/lint/post-synth/dwdc010.ts +62 -0
  135. package/src/lint/post-synth/dwdc011.ts +61 -0
  136. package/src/lint/post-synth/dwdc012.ts +46 -0
  137. package/src/lint/post-synth/dwdc013.ts +64 -0
  138. package/src/lint/post-synth/dwde-post-synth.test.ts +368 -0
  139. package/src/lint/post-synth/dwde010.ts +130 -0
  140. package/src/lint/post-synth/dwde011.ts +108 -0
  141. package/src/lint/post-synth/dwds010.ts +46 -0
  142. package/src/lint/post-synth/index.ts +14 -0
  143. package/src/lint/post-synth/post-synth.test.ts +7 -3
  144. package/src/op/activities/index.ts +27 -0
  145. package/src/plugin.test.ts +3 -2
  146. package/src/plugin.ts +30 -0
  147. package/src/policy-text.ts +128 -0
  148. package/src/serializer.ts +71 -109
  149. package/src/skills/chant-cedar-dogwood.md +327 -0
@@ -0,0 +1,254 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { checkParsePolicySet } from "@cedar-policy/cedar-wasm/nodejs";
3
+ import { build } from "@intentius/chant/build";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import {
7
+ AGENTCORE_STATEMENT_MAX,
8
+ agentCorePolicyDefinition,
9
+ agentCorePolicyName,
10
+ agentCorePolicyResource,
11
+ agentCorePolicySet,
12
+ agentCoreStagedPolicy,
13
+ agentCoreStatement,
14
+ } from "./embed";
15
+ import { cedarSerializer, type CedarPolicyProps } from "../serializer";
16
+ import { DOGWOOD_POLICY_FILENAME, TemporalPolicy, type TemporalPolicyProps } from "../dogwood/policy";
17
+ import { Document, Policy } from "../generated/index";
18
+ import { ctx, formerly, predicate } from "../dogwood/temporal";
19
+
20
+ const exampleDir = join(dirname(fileURLToPath(import.meta.url)), "../../examples/agentcore-policy/src");
21
+
22
+ const denyWrite: CedarPolicyProps = {
23
+ effect: "forbid",
24
+ principal: { is: "App::ServiceAccount" },
25
+ action: { eq: 'App::Action::"write"' },
26
+ resource: { is: "App::Document" },
27
+ unless: ["context.authenticated == true"],
28
+ };
29
+
30
+ const needsApproval: TemporalPolicyProps = {
31
+ effect: "permit",
32
+ principal: { is: "App::ServiceAccount" },
33
+ action: { eq: 'App::Action::"write"' },
34
+ resource: { is: "App::Document" },
35
+ whenTemporal: [
36
+ formerly("1h", predicate('App::Action::"approve"', "response", { "input.document": ctx("input.document") })),
37
+ ],
38
+ };
39
+
40
+ async function buildExample() {
41
+ const result = await build(exampleDir, [cedarSerializer]);
42
+ expect(result.errors).toEqual([]);
43
+ const output = result.outputs.get("cedar");
44
+ if (typeof output === "string" || output === undefined) throw new Error("expected a multi-file result");
45
+ return { result, cedarText: output.primary, dwText: output.files?.[DOGWOOD_POLICY_FILENAME] ?? "" };
46
+ }
47
+
48
+ // ── The statement ─────────────────────────────────────────────────
49
+
50
+ describe("the AgentCore statement", () => {
51
+ it("is Cedar the real parser accepts, for the plain-Cedar arm", () => {
52
+ const statement = agentCoreStatement("denyWrite", denyWrite);
53
+ expect(checkParsePolicySet({ staticPolicies: statement }).type).toBe("success");
54
+ });
55
+
56
+ it("carries the @id the live policy is matched back on", () => {
57
+ expect(agentCoreStatement("denyWrite", denyWrite)).toContain('@id("deny-write")');
58
+ expect(agentCoreStatement("denyWrite", denyWrite, { policyId: "override" })).toContain('@id("override")');
59
+ });
60
+
61
+ it("renders a temporal policy as .dw text", () => {
62
+ const statement = agentCoreStatement("needsApproval", needsApproval);
63
+ expect(statement).toContain("when temporal {");
64
+ expect(statement).toContain('formerly within 1h App::Action::"approve"::response{');
65
+ });
66
+
67
+ it("is byte-identical to what the serializer writes to .cedar", async () => {
68
+ const { result, cedarText } = await buildExample();
69
+ const set = agentCorePolicySet(result.entities);
70
+
71
+ const cedarArm = Object.values(set).filter((d) => d.Cedar !== undefined);
72
+ expect(cedarArm.length).toBeGreaterThan(0);
73
+ for (const definition of cedarArm) {
74
+ expect(cedarText).toContain(definition.Cedar!.Statement);
75
+ }
76
+ });
77
+
78
+ it("is byte-identical to what the serializer writes to policies.dw", async () => {
79
+ const { result, dwText } = await buildExample();
80
+ const set = agentCorePolicySet(result.entities);
81
+
82
+ const policyArm = Object.values(set).filter((d) => d.Policy !== undefined);
83
+ expect(policyArm.length).toBe(2);
84
+ for (const definition of policyArm) {
85
+ expect(dwText).toContain(definition.Policy!.Statement);
86
+ }
87
+ });
88
+
89
+ it("refuses a template — AgentCore statements are static", () => {
90
+ expect(() =>
91
+ agentCoreStatement("ownerOnly", {
92
+ effect: "permit",
93
+ principal: { eq: "?principal" },
94
+ action: { eq: 'App::Action::"read"' },
95
+ resource: { is: "App::Document" },
96
+ }),
97
+ ).toThrow(/carries a Cedar template slot/);
98
+
99
+ expect(() =>
100
+ agentCoreStatement("resourceSlot", {
101
+ effect: "permit",
102
+ principal: { is: "App::User" },
103
+ action: { eq: 'App::Action::"read"' },
104
+ resource: { eq: "?resource" },
105
+ }),
106
+ ).toThrow(/no template-linked arm/);
107
+ });
108
+
109
+ it("names the declaration when there is nothing to embed", () => {
110
+ expect(() => agentCoreStatement("empty", new Map())).toThrow(/rendered no policy text/);
111
+ });
112
+
113
+ it("refuses a statement past AgentCore's own length cap", () => {
114
+ const long = { ...denyWrite, unless: ["context.authenticated == true".padEnd(AGENTCORE_STATEMENT_MAX, " ")] };
115
+ expect(() => agentCoreStatement("tooLong", long)).toThrow(/maximum is 10000/);
116
+ });
117
+ });
118
+
119
+ // ── The Definition union ──────────────────────────────────────────
120
+
121
+ describe("the AgentCore Definition union", () => {
122
+ it("puts plain Cedar in the Cedar arm and nothing else", () => {
123
+ const definition = agentCorePolicyDefinition("denyWrite", denyWrite);
124
+ expect(Object.keys(definition)).toEqual(["Cedar"]);
125
+ expect(typeof definition.Cedar!.Statement).toBe("string");
126
+ });
127
+
128
+ it("puts temporal text in the language-agnostic Policy arm and nothing else", () => {
129
+ const definition = agentCorePolicyDefinition("needsApproval", needsApproval);
130
+ expect(Object.keys(definition)).toEqual(["Policy"]);
131
+ expect(definition.Policy!.Statement).toContain("when temporal {");
132
+ });
133
+
134
+ it("reads the entity type off a declared TemporalPolicy", () => {
135
+ const definition = agentCorePolicyDefinition("needsApproval", new TemporalPolicy(needsApproval));
136
+ expect(Object.keys(definition)).toEqual(["Policy"]);
137
+ });
138
+
139
+ it("reads the entity type off a declared Cedar Policy", () => {
140
+ // Built against the generated class rather than spread from `denyWrite`:
141
+ // `PolicyProps` narrows every scope to the schema's own entity and action
142
+ // names, which `CedarPolicyProps` leaves as strings.
143
+ const declared = new Policy({
144
+ effect: "forbid",
145
+ principal: { is: "App::ServiceAccount" },
146
+ action: { eq: 'App::Action::"write"' },
147
+ resource: { is: "App::Document" },
148
+ unless: ["context.authenticated == true"],
149
+ });
150
+ const definition = agentCorePolicyDefinition("denyWrite", declared);
151
+ expect(Object.keys(definition)).toEqual(["Cedar"]);
152
+ expect(definition.Cedar!.Statement).toBe(agentCoreStatement("denyWrite", denyWrite));
153
+ });
154
+
155
+ it("refuses a declared entity that is not a policy at all", () => {
156
+ const document = new Document({ owner: 'App::User::"alice"' } as never);
157
+ expect(() => agentCorePolicyDefinition("doc", document)).toThrow(/which is not a policy/);
158
+ });
159
+
160
+ it("lets plain Cedar be forced into the Policy arm, because dogwood embeds Cedar", () => {
161
+ const definition = agentCorePolicyDefinition("denyWrite", denyWrite, { language: "dogwood" });
162
+ expect(Object.keys(definition)).toEqual(["Policy"]);
163
+ expect(definition.Policy!.Statement).toContain("forbid");
164
+ });
165
+
166
+ it("refuses to force temporal text into the Cedar arm", () => {
167
+ expect(() => agentCorePolicyDefinition("needsApproval", needsApproval, { language: "cedar" })).toThrow(
168
+ /has no `when temporal` rule/,
169
+ );
170
+ });
171
+
172
+ it("takes a whole policy set and lands it in the Policy arm when any policy is temporal", async () => {
173
+ const { result, cedarText, dwText } = await buildExample();
174
+ const definition = agentCorePolicyDefinition("gateway", result.entities);
175
+
176
+ expect(Object.keys(definition)).toEqual(["Policy"]);
177
+ // Cedar first, then temporal, both verbatim from the emitted artifacts.
178
+ expect(definition.Policy!.Statement).toContain(cedarText.trimEnd());
179
+ for (const line of ['@id("write-needs-approval")', '@id("session-spend-budget")']) {
180
+ expect(definition.Policy!.Statement).toContain(line);
181
+ expect(dwText).toContain(line);
182
+ }
183
+ });
184
+ });
185
+
186
+ // ── The resource props ────────────────────────────────────────────
187
+
188
+ describe("the resource form", () => {
189
+ it("fills every required prop of the generated class", () => {
190
+ const resource = agentCorePolicyResource("denyWrite", denyWrite, "GatewayEngine-abcdefghij");
191
+ expect(resource.PolicyEngineId).toBe("GatewayEngine-abcdefghij");
192
+ expect(resource.Name).toBe("denyWrite");
193
+ expect(resource.Definition.Cedar!.Statement).toContain("forbid");
194
+ expect(resource.EnforcementMode).toBeUndefined();
195
+ });
196
+
197
+ it("carries the stage and the description when asked", () => {
198
+ const resource = agentCorePolicyResource("denyWrite", denyWrite, "engine", {
199
+ stage: "log-only",
200
+ description: "watch it first",
201
+ });
202
+ expect(resource.EnforcementMode).toBe("LOG_ONLY");
203
+ expect(resource.Description).toBe("watch it first");
204
+ });
205
+
206
+ it("checks Name against AgentCore's create-only pattern instead of coercing it", () => {
207
+ expect(agentCorePolicyName("writeNeedsApproval")).toBe("writeNeedsApproval");
208
+ expect(() => agentCorePolicyName("write-needs-approval")).toThrow(/not a legal/);
209
+ expect(() => agentCorePolicyName("9lives")).toThrow(/not a legal/);
210
+ expect(() => agentCorePolicyName("a".repeat(49))).toThrow(/maximum is 48/);
211
+ });
212
+ });
213
+
214
+ // ── Staging ───────────────────────────────────────────────────────
215
+
216
+ describe("the staged form", () => {
217
+ it("is the three props the generated class needs beside PolicyEngineId", () => {
218
+ const staged = agentCoreStagedPolicy("needsApproval", needsApproval, "log-only");
219
+ expect(staged.Name).toBe("needsApproval");
220
+ expect(staged.EnforcementMode).toBe("LOG_ONLY");
221
+ expect(staged.Definition.Policy!.Statement).toContain("when temporal {");
222
+ });
223
+
224
+ it("promotes on one token", () => {
225
+ const observed = agentCoreStagedPolicy("needsApproval", needsApproval, "log-only");
226
+ const enforced = agentCoreStagedPolicy("needsApproval", needsApproval, "enforce");
227
+ expect(enforced.EnforcementMode).toBe("ACTIVE");
228
+ expect(enforced.Definition).toEqual(observed.Definition);
229
+ });
230
+ });
231
+
232
+ // ── The whole-set form ────────────────────────────────────────────
233
+
234
+ describe("the whole-set form", () => {
235
+ it("keys one definition per policy by chant entity name", async () => {
236
+ const { result } = await buildExample();
237
+ const set = agentCorePolicySet(result.entities);
238
+ expect(Object.keys(set).sort()).toEqual([
239
+ "denyUnauthenticatedWrite",
240
+ "sessionSpendBudget",
241
+ "writeNeedsApproval",
242
+ ]);
243
+ });
244
+
245
+ it("keeps each policy on its own arm, so EnforcementMode stays per policy", async () => {
246
+ const { result } = await buildExample();
247
+ const set = agentCorePolicySet(result.entities);
248
+ expect(Object.keys(set.denyUnauthenticatedWrite)).toEqual(["Cedar"]);
249
+ expect(Object.keys(set.writeNeedsApproval)).toEqual(["Policy"]);
250
+ expect(checkParsePolicySet({ staticPolicies: set.denyUnauthenticatedWrite.Cedar!.Statement }).type).toBe(
251
+ "success",
252
+ );
253
+ });
254
+ });
@@ -0,0 +1,399 @@
1
+ /**
2
+ * Typed embedding of a cedar or dogwood policy into
3
+ * `AWS::BedrockAgentCore::Policy` (#1660).
4
+ *
5
+ * The aws lexicon keeps the deployment vehicle, exactly as it does for AVP
6
+ * (#1652, `../avp/embed.ts`). What differs is the shape of the vehicle:
7
+ * AgentCore's `Definition` is a two-arm `oneOf`, not the single `Static` arm
8
+ * `AWS::VerifiedPermissions::Policy` has —
9
+ *
10
+ * ```jsonc
11
+ * "PolicyDefinition": {
12
+ * "oneOf": [{ "required": ["Cedar"] }, { "required": ["Policy"] }]
13
+ * }
14
+ * ```
15
+ *
16
+ * — where `Cedar.Statement` is plain Cedar and `Policy.Statement` is the
17
+ * language-agnostic arm. That second arm is what makes AgentCore the
18
+ * deployment target for the dogwood dialect: a `.dw` policy is a Cedar policy
19
+ * with `when temporal { … }` clauses, which the `Cedar` arm has no business
20
+ * accepting, and the `Policy` arm exists precisely so a policy engine can be
21
+ * handed something that is not plain Cedar.
22
+ *
23
+ * A project that has both lexicons installed writes
24
+ *
25
+ * ```ts
26
+ * new BedrockAgentCorePolicy({
27
+ * PolicyEngineId: engine.ref(),
28
+ * Name: "writeNeedsApproval",
29
+ * ...agentCoreStagedPolicy("writeNeedsApproval", writeNeedsApproval, "log-only"),
30
+ * });
31
+ * ```
32
+ *
33
+ * and the statement is the same text `chant build` writes to `policies.dw`,
34
+ * rendered by the same renderer, with `@id` intact.
35
+ *
36
+ * ### Why no dependency, and why the example uses plain objects
37
+ *
38
+ * Same rule as `../avp/embed.ts` and for the same reason: a cedar → aws
39
+ * dependency would invert the epic's decision that Cedar is vendor-neutral and
40
+ * make the cedar lexicon unbuildable without the aws one. Nothing here imports
41
+ * `@intentius/chant-lexicon-aws`. The seam is the data shape, which is stable
42
+ * CloudFormation, and the prop names below are the generated class's own
43
+ * (`PolicyEngineId`, `Name`, `Definition`, `EnforcementMode`, `Description`),
44
+ * so substituting the real class is a one-line edit. `examples/agentcore-policy/`
45
+ * demonstrates the pairing with a plain-object stand-in, because the shipped
46
+ * cedar examples build against the cedar serializer alone and an `AWS::*` entity
47
+ * declared in that tree would have no serializer to emit it.
48
+ *
49
+ * ### What this module refuses
50
+ *
51
+ * Templates. `AWS::VerifiedPermissions::Policy` has a `TemplateLinked` arm;
52
+ * AgentCore's `Definition` union does not, so a statement carrying a
53
+ * `?principal` or `?resource` slot has nowhere to go and would be rejected at
54
+ * deploy time with an error about the policy text rather than about the slot.
55
+ * {@link agentCoreStatement} throws instead, at authoring time, naming the
56
+ * declaration.
57
+ */
58
+
59
+ import { isDeclarable, type Declarable } from "@intentius/chant/declarable";
60
+ import {
61
+ cedarPolicyRecords,
62
+ getProps,
63
+ renderPolicyText,
64
+ resolvePolicyId,
65
+ CEDAR_POLICY_TYPE,
66
+ type CedarPolicyProps,
67
+ } from "../serializer";
68
+ import { dogwoodPolicyRecords, renderTemporalPolicyText } from "../dogwood/serialize";
69
+ import { DOGWOOD_POLICY_TYPE, type TemporalPolicyProps } from "../dogwood/policy";
70
+ import { enforcementMode, type AgentCoreEnforcementMode, type AgentCoreStage } from "./enforcement";
71
+
72
+ // ── The CloudFormation shapes ─────────────────────────────────────
73
+
74
+ /** `Definition.Cedar` — the plain-Cedar arm of the definition union. */
75
+ export interface AgentCoreCedarDefinition {
76
+ Statement: string;
77
+ }
78
+
79
+ /** `Definition.Policy` — the language-agnostic arm, which carries `.dw` text. */
80
+ export interface AgentCoreLanguageDefinition {
81
+ Statement: string;
82
+ }
83
+
84
+ /**
85
+ * The `Definition` property of `AWS::BedrockAgentCore::Policy`.
86
+ *
87
+ * A union rather than a record with two optional keys, because upstream's
88
+ * schema is a `oneOf`: a definition carrying both arms is rejected, and a type
89
+ * that can express it is a type that lets a caller build one.
90
+ */
91
+ export type AgentCorePolicyDefinition =
92
+ | { Cedar: AgentCoreCedarDefinition; Policy?: never }
93
+ | { Policy: AgentCoreLanguageDefinition; Cedar?: never };
94
+
95
+ /** The props of `AWS::BedrockAgentCore::Policy` this module can fill. */
96
+ export interface AgentCorePolicyResource {
97
+ PolicyEngineId: string;
98
+ Name: string;
99
+ Definition: AgentCorePolicyDefinition;
100
+ EnforcementMode?: AgentCoreEnforcementMode;
101
+ Description?: string;
102
+ }
103
+
104
+ /**
105
+ * A policy paired with its rollout stage — everything but the engine id.
106
+ *
107
+ * Spreadable into the generated class, which is the point: the caller supplies
108
+ * `PolicyEngineId` (an AttrRef this module has no type for) and this supplies
109
+ * the three props that come from the declaration.
110
+ */
111
+ export interface AgentCoreStagedPolicy {
112
+ Name: string;
113
+ Definition: AgentCorePolicyDefinition;
114
+ EnforcementMode: AgentCoreEnforcementMode;
115
+ Description?: string;
116
+ }
117
+
118
+ // ── Upstream's own limits ─────────────────────────────────────────
119
+
120
+ /** `Statement` minLength, on both arms of the union. */
121
+ export const AGENTCORE_STATEMENT_MIN = 35;
122
+
123
+ /** `Statement` maxLength, on both arms of the union. */
124
+ export const AGENTCORE_STATEMENT_MAX = 10000;
125
+
126
+ /** `Name` — an identifier, and create-only, so a rename replaces the policy. */
127
+ export const AGENTCORE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/;
128
+
129
+ /**
130
+ * `?principal` / `?resource` — Cedar's two template slots.
131
+ *
132
+ * Scanned over the whole statement rather than only the scope positions, which
133
+ * is where a slot can legally appear. Inside a `temporal { … }` clause the `?`
134
+ * sigil means a macro parameter, so in principle a macro parameter named
135
+ * `?principal` would read as a slot here — but macro parameters are legal only
136
+ * inside a macro *definition* body, and this module renders policies, never
137
+ * definitions. There is nowhere in a policy statement for the pattern to mean
138
+ * anything else.
139
+ */
140
+ const TEMPLATE_SLOT = /\?(?:principal|resource)\b/;
141
+
142
+ // ── Options ───────────────────────────────────────────────────────
143
+
144
+ /** What a caller can hand the embedding: one policy, or a whole build's worth. */
145
+ export type AgentCorePolicySource =
146
+ | Declarable
147
+ | CedarPolicyProps
148
+ | TemporalPolicyProps
149
+ | Record<string, unknown>
150
+ | Map<string, Declarable>;
151
+
152
+ export interface AgentCoreEmbedOptions {
153
+ /** Override the policy id. Defaults to the serializer's own derivation. */
154
+ policyId?: string;
155
+ /**
156
+ * Force an arm of the `Definition` union.
157
+ *
158
+ * The default reads the policy: temporal clauses take the `Policy` arm, plain
159
+ * Cedar takes the `Cedar` arm. Forcing `"dogwood"` on plain Cedar is
160
+ * legitimate — dogwood embeds Cedar, so the `Policy` arm accepts it, and a
161
+ * policy set that will grow temporal clauses need not change arms later.
162
+ * Forcing `"cedar"` on temporal text is not, and throws.
163
+ */
164
+ language?: "cedar" | "dogwood";
165
+ /** `Description` on the resource. Free prose; AgentCore does nothing with it. */
166
+ description?: string;
167
+ }
168
+
169
+ // ── Rendering ─────────────────────────────────────────────────────
170
+
171
+ /** The dialect keys `TemporalPolicyProps` adds to `CedarPolicyProps`. */
172
+ const DOGWOOD_ONLY_PROPS = ["whenTemporal", "unlessTemporal", "whenGuardrails", "unlessGuardrails"] as const;
173
+
174
+ function hasTemporalProps(props: Record<string, unknown>): boolean {
175
+ return DOGWOOD_ONLY_PROPS.some((key) => {
176
+ const value = props[key];
177
+ return Array.isArray(value) ? value.length > 0 : value !== undefined;
178
+ });
179
+ }
180
+
181
+ /** One rendered statement, and which arm it belongs in. */
182
+ interface RenderedStatement {
183
+ text: string;
184
+ /** True when the text is `.dw` rather than plain Cedar. */
185
+ temporal: boolean;
186
+ }
187
+
188
+ function renderRecord(name: string, props: Record<string, unknown>, options: AgentCoreEmbedOptions): RenderedStatement {
189
+ const temporal = hasTemporalProps(props);
190
+ const id = options.policyId ?? resolvePolicyId(name, props);
191
+ return { text: temporal ? renderTemporalPolicyText(id, props) : renderPolicyText(id, props), temporal };
192
+ }
193
+
194
+ /**
195
+ * Every policy in a build, as one statement.
196
+ *
197
+ * Cedar policies first, then temporal ones, both in declaration order, so the
198
+ * text is stable across builds. Any temporal policy in the set puts the whole
199
+ * statement in the `Policy` arm — which is correct rather than a compromise: a
200
+ * `.dw` file holds plain Cedar policies too, and splitting the set across two
201
+ * AgentCore policies to keep one of them in the `Cedar` arm would give the two
202
+ * halves separate `EnforcementMode` dials for no reason.
203
+ */
204
+ function renderPolicySet(entities: Map<string, Declarable>): RenderedStatement {
205
+ const parts: string[] = [];
206
+ for (const { id, props } of cedarPolicyRecords(entities)) parts.push(renderPolicyText(id, props));
207
+
208
+ const temporalRecords = dogwoodPolicyRecords(entities);
209
+ for (const { id, props } of temporalRecords) parts.push(renderTemporalPolicyText(id, props));
210
+
211
+ return { text: parts.join("\n\n"), temporal: temporalRecords.length > 0 };
212
+ }
213
+
214
+ function render(
215
+ name: string,
216
+ source: AgentCorePolicySource,
217
+ options: AgentCoreEmbedOptions,
218
+ ): RenderedStatement {
219
+ if (source instanceof Map) return renderPolicySet(source);
220
+
221
+ if (isDeclarable(source)) {
222
+ if (source.entityType !== CEDAR_POLICY_TYPE && source.entityType !== DOGWOOD_POLICY_TYPE) {
223
+ throw new Error(
224
+ `cedar: "${name}" is a ${source.entityType}, which is not a policy — AWS::BedrockAgentCore::Policy embeds ${CEDAR_POLICY_TYPE} or ${DOGWOOD_POLICY_TYPE}.`,
225
+ );
226
+ }
227
+ // Read straight off the entity: no reference walk, because a single
228
+ // Declarable carries no map of the names its siblings were declared under.
229
+ // A scope that names another declared entity needs the policy-set form,
230
+ // which walks references the way the serializer does.
231
+ const props = getProps(source);
232
+ const temporal = source.entityType === DOGWOOD_POLICY_TYPE || hasTemporalProps(props);
233
+ const id = options.policyId ?? resolvePolicyId(name, props);
234
+ return { text: temporal ? renderTemporalPolicyText(id, props) : renderPolicyText(id, props), temporal };
235
+ }
236
+
237
+ return renderRecord(name, source as Record<string, unknown>, options);
238
+ }
239
+
240
+ function assertEmbeddable(name: string, statement: RenderedStatement): void {
241
+ if (statement.text.length === 0) {
242
+ throw new Error(`cedar: "${name}" rendered no policy text, so there is nothing for AgentCore to evaluate.`);
243
+ }
244
+ if (TEMPLATE_SLOT.test(statement.text)) {
245
+ throw new Error(
246
+ `cedar: the statement for "${name}" carries a Cedar template slot — AWS::BedrockAgentCore::Policy takes a static statement, and its Definition union has no template-linked arm. Write the ?principal/?resource slot as a concrete entity, or deploy the template through AWS::VerifiedPermissions::Policy instead.`,
247
+ );
248
+ }
249
+ if (statement.text.length < AGENTCORE_STATEMENT_MIN) {
250
+ throw new Error(
251
+ `cedar: the statement for "${name}" is ${statement.text.length} characters and AgentCore's minimum is ${AGENTCORE_STATEMENT_MIN}.`,
252
+ );
253
+ }
254
+ if (statement.text.length > AGENTCORE_STATEMENT_MAX) {
255
+ throw new Error(
256
+ `cedar: the statement for "${name}" is ${statement.text.length} characters and AgentCore's maximum is ${AGENTCORE_STATEMENT_MAX} — split it across policies, each with its own EnforcementMode.`,
257
+ );
258
+ }
259
+ }
260
+
261
+ // ── The seam ──────────────────────────────────────────────────────
262
+
263
+ /**
264
+ * The policy text for one AgentCore policy — the exact string a `Statement`
265
+ * wants, on whichever arm it lands.
266
+ *
267
+ * `name` is the chant entity name; the `@id` annotation the statement carries
268
+ * is derived from it the same way the serializer derives it, so the statement
269
+ * in the policy engine can be matched back to the declaration that produced it.
270
+ */
271
+ export function agentCoreStatement(
272
+ name: string,
273
+ source: AgentCorePolicySource,
274
+ options: AgentCoreEmbedOptions = {},
275
+ ): string {
276
+ const statement = render(name, source, options);
277
+ assertEmbeddable(name, statement);
278
+ return statement.text;
279
+ }
280
+
281
+ /**
282
+ * The `Definition` property of `AWS::BedrockAgentCore::Policy`.
283
+ *
284
+ * The arm is chosen by what the policy is: a `Dogwood::TemporalPolicy`, or any
285
+ * props carrying a temporal clause, lands in `Definition.Policy`; plain Cedar
286
+ * lands in `Definition.Cedar`. `options.language` overrides in the one
287
+ * direction that is safe (see {@link AgentCoreEmbedOptions.language}).
288
+ */
289
+ export function agentCorePolicyDefinition(
290
+ name: string,
291
+ source: AgentCorePolicySource,
292
+ options: AgentCoreEmbedOptions = {},
293
+ ): AgentCorePolicyDefinition {
294
+ const statement = render(name, source, options);
295
+ assertEmbeddable(name, statement);
296
+
297
+ if (options.language === "cedar" && statement.temporal) {
298
+ throw new Error(
299
+ `cedar: "${name}" has temporal clauses, so it cannot go in Definition.Cedar — that arm is plain Cedar, and AgentCore's Cedar parser has no \`when temporal\` rule. Drop \`language: "cedar"\` and it lands in Definition.Policy.`,
300
+ );
301
+ }
302
+
303
+ const dogwood = options.language === "dogwood" || (options.language === undefined && statement.temporal);
304
+ return dogwood ? { Policy: { Statement: statement.text } } : { Cedar: { Statement: statement.text } };
305
+ }
306
+
307
+ /**
308
+ * A policy and its rollout stage, ready to spread into the generated class.
309
+ *
310
+ * The staged-rollout pattern in one call, which is the point (#1660, and the
311
+ * loomster#171 showcase seam): shipping log-only and promoting to enforcing is
312
+ * a one-token diff, not a hand-edited `EnforcementMode` string somewhere else
313
+ * in the template. See ./enforcement.ts for why observe-before-enforce is the
314
+ * default reading of a temporal policy rather than an optional nicety.
315
+ */
316
+ export function agentCoreStagedPolicy(
317
+ name: string,
318
+ source: AgentCorePolicySource,
319
+ stage: AgentCoreStage,
320
+ options: AgentCoreEmbedOptions = {},
321
+ ): AgentCoreStagedPolicy {
322
+ return {
323
+ Name: agentCorePolicyName(name),
324
+ Definition: agentCorePolicyDefinition(name, source, options),
325
+ EnforcementMode: enforcementMode(stage),
326
+ ...(options.description ? { Description: options.description } : {}),
327
+ };
328
+ }
329
+
330
+ /**
331
+ * Every required prop of `AWS::BedrockAgentCore::Policy`, plus the stage.
332
+ *
333
+ * `PolicyEngineId` is a string here rather than an AttrRef because this module
334
+ * does not know the aws lexicon's reference types. A project passes
335
+ * `engine.ref()` in place of the literal and TypeScript is satisfied by the
336
+ * generated class's own prop type, not by this one.
337
+ */
338
+ export function agentCorePolicyResource(
339
+ name: string,
340
+ source: AgentCorePolicySource,
341
+ policyEngineId: string,
342
+ options: AgentCoreEmbedOptions & { stage?: AgentCoreStage } = {},
343
+ ): AgentCorePolicyResource {
344
+ const { stage, ...embed } = options;
345
+ return {
346
+ PolicyEngineId: policyEngineId,
347
+ Name: agentCorePolicyName(name),
348
+ Definition: agentCorePolicyDefinition(name, source, embed),
349
+ ...(stage ? { EnforcementMode: enforcementMode(stage) } : {}),
350
+ ...(embed.description ? { Description: embed.description } : {}),
351
+ };
352
+ }
353
+
354
+ /**
355
+ * Every policy in a build, as one AgentCore `Definition` each, keyed by chant
356
+ * entity name.
357
+ *
358
+ * The whole-set form, and the one that walks references between declared
359
+ * entities — that is what `cedarPolicyRecords`/`dogwoodPolicyRecords` do. One
360
+ * definition per policy rather than one merged statement, because
361
+ * `EnforcementMode` is a per-policy dial and merging would throw it away; hand
362
+ * the map to {@link agentCorePolicyDefinition} instead when a single AgentCore
363
+ * policy really should carry the whole set.
364
+ */
365
+ export function agentCorePolicySet(
366
+ entities: Map<string, Declarable>,
367
+ options: AgentCoreEmbedOptions = {},
368
+ ): Record<string, AgentCorePolicyDefinition> {
369
+ const out: Record<string, AgentCorePolicyDefinition> = {};
370
+
371
+ for (const { name, id, props } of cedarPolicyRecords(entities)) {
372
+ out[name] = agentCorePolicyDefinition(name, props, { ...options, policyId: id });
373
+ }
374
+ for (const { name, id, props } of dogwoodPolicyRecords(entities)) {
375
+ out[name] = agentCorePolicyDefinition(name, props, { ...options, policyId: id, language: "dogwood" });
376
+ }
377
+
378
+ return out;
379
+ }
380
+
381
+ /**
382
+ * The chant entity name as an AgentCore `Name`.
383
+ *
384
+ * `Name` is create-only and matches `^[A-Za-z][A-Za-z0-9_]*$` — no hyphens,
385
+ * which is why it is not the kebab-case policy id the `@id` annotation carries.
386
+ * Checked rather than coerced: silently rewriting a name that is create-only
387
+ * would mean a later rename replaces the policy without anyone having asked.
388
+ */
389
+ export function agentCorePolicyName(name: string): string {
390
+ if (!AGENTCORE_NAME_PATTERN.test(name)) {
391
+ throw new Error(
392
+ `cedar: "${name}" is not a legal AWS::BedrockAgentCore::Policy Name — it must match ${AGENTCORE_NAME_PATTERN.source} (letters, digits and underscores, starting with a letter).`,
393
+ );
394
+ }
395
+ if (name.length > 48) {
396
+ throw new Error(`cedar: the AgentCore policy Name "${name}" is ${name.length} characters and the maximum is 48.`);
397
+ }
398
+ return name;
399
+ }