@intentius/chant-lexicon-cedar 0.124.31 → 0.124.32

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 (101) hide show
  1. package/dist/agentcore/embed.js +250 -0
  2. package/dist/agentcore/enforcement.js +81 -0
  3. package/dist/agentcore/scan.js +93 -0
  4. package/dist/avp/ambient.js +100 -0
  5. package/dist/avp/client.js +318 -0
  6. package/dist/avp/describe-resources.js +144 -0
  7. package/dist/avp/embed.js +125 -0
  8. package/dist/avp/live-export.js +151 -0
  9. package/dist/avp/ownership.js +144 -0
  10. package/dist/avp/statement.js +45 -0
  11. package/dist/avp/store.js +105 -0
  12. package/dist/avp/testdata/mock-transport.js +100 -0
  13. package/dist/codegen/docs-cli.js +6 -0
  14. package/dist/codegen/docs.js +133 -0
  15. package/dist/codegen/emit.js +331 -0
  16. package/dist/codegen/generate-cli.js +18 -0
  17. package/dist/codegen/generate.js +122 -0
  18. package/dist/codegen/naming.js +90 -0
  19. package/dist/codegen/package.js +33 -0
  20. package/dist/commands.js +86 -0
  21. package/dist/composites/deny-by-default-set.js +51 -0
  22. package/dist/composites/index.js +9 -0
  23. package/dist/composites/owner-can-manage.js +48 -0
  24. package/dist/config.js +159 -0
  25. package/dist/coverage.js +112 -0
  26. package/dist/detect.js +61 -0
  27. package/dist/dogwood/cli.js +477 -0
  28. package/dist/dogwood/event-schema.js +219 -0
  29. package/dist/dogwood/index.js +32 -0
  30. package/dist/dogwood/macros.js +175 -0
  31. package/dist/dogwood/policy.js +58 -0
  32. package/dist/dogwood/replay-activity.js +313 -0
  33. package/dist/dogwood/replay-op.js +134 -0
  34. package/dist/dogwood/scan.js +234 -0
  35. package/dist/dogwood/serialize.js +199 -0
  36. package/dist/dogwood/temporal.js +354 -0
  37. package/dist/dogwood/trace.js +269 -0
  38. package/dist/dogwood/upstream.js +40 -0
  39. package/dist/dogwood/window.js +82 -0
  40. package/dist/gate-policy-pack.js +189 -0
  41. package/dist/gate-policy.js +168 -0
  42. package/dist/gate-schema.js +122 -0
  43. package/dist/generated/index.js +94 -0
  44. package/dist/generated/runtime.js +2 -0
  45. package/dist/import/adapter.js +54 -0
  46. package/dist/import/clause-text.js +166 -0
  47. package/dist/import/generator.js +178 -0
  48. package/dist/import/parser.js +273 -0
  49. package/dist/index.js +79 -0
  50. package/dist/init-templates.js +330 -0
  51. package/dist/integrity.json +2 -2
  52. package/dist/lint/audit-catalog.js +65 -0
  53. package/dist/lint/audit-lineage.js +23 -0
  54. package/dist/lint/post-synth/cedar-helpers.js +123 -0
  55. package/dist/lint/post-synth/cedc010.js +59 -0
  56. package/dist/lint/post-synth/cedc011.js +29 -0
  57. package/dist/lint/post-synth/cedc012.js +34 -0
  58. package/dist/lint/post-synth/cedc013.js +45 -0
  59. package/dist/lint/post-synth/cedc014.js +54 -0
  60. package/dist/lint/post-synth/cede010.js +51 -0
  61. package/dist/lint/post-synth/cede011.js +36 -0
  62. package/dist/lint/post-synth/ceds010.js +31 -0
  63. package/dist/lint/post-synth/ceds011.js +22 -0
  64. package/dist/lint/post-synth/ceds012.js +31 -0
  65. package/dist/lint/post-synth/dogwood-helpers.js +94 -0
  66. package/dist/lint/post-synth/dwdc010.js +38 -0
  67. package/dist/lint/post-synth/dwdc011.js +34 -0
  68. package/dist/lint/post-synth/dwdc012.js +24 -0
  69. package/dist/lint/post-synth/dwdc013.js +29 -0
  70. package/dist/lint/post-synth/dwde010.js +84 -0
  71. package/dist/lint/post-synth/dwde011.js +67 -0
  72. package/dist/lint/post-synth/dwds010.js +21 -0
  73. package/dist/lint/post-synth/index.js +36 -0
  74. package/dist/lint/post-synth/wasm-helpers.js +234 -0
  75. package/dist/lint/rules/index.js +9 -0
  76. package/dist/lint/rules/policy-shape.js +114 -0
  77. package/dist/lsp/completions.js +52 -0
  78. package/dist/lsp/hover.js +56 -0
  79. package/dist/lsp/registry.d.ts.map +1 -1
  80. package/dist/lsp/registry.js +51 -0
  81. package/dist/manifest.json +1 -1
  82. package/dist/mcp/index.js +82 -0
  83. package/dist/mcp/policy-coverage.js +134 -0
  84. package/dist/op/activities/index.js +16 -0
  85. package/dist/package-cli.js +18 -0
  86. package/dist/plugin.js +304 -0
  87. package/dist/policy-text.js +117 -0
  88. package/dist/schema-artifact.js +66 -0
  89. package/dist/serializer.js +203 -0
  90. package/dist/spec/fetch.js +96 -0
  91. package/dist/spec/parse.js +164 -0
  92. package/dist/spec/pin.js +170 -0
  93. package/dist/spec/wasm.js +194 -0
  94. package/dist/validate-cli.js +7 -0
  95. package/dist/validate.js +74 -0
  96. package/package.json +7 -7
  97. package/src/codegen/package.ts +2 -2
  98. package/src/coverage.ts +2 -2
  99. package/src/import/generator.ts +2 -2
  100. package/src/lsp/registry.ts +2 -1
  101. package/src/spec/fetch.ts +2 -2
@@ -0,0 +1,134 @@
1
+ /**
2
+ * `PolicyReplayOp` — replay a declared `.dw` set against a recorded event
3
+ * trace and report divergence (#1661, epic #1646).
4
+ *
5
+ * Shape follows `WorkflowAuditOp`: an observe-dial Op whose last phase is a
6
+ * finding mode (`report | issue | pull-request`). What it observes is not a
7
+ * moving upstream but a moving *history* — a policy set that was correct
8
+ * against last week's traffic can decide differently against this week's, and
9
+ * the deterministic build cannot see that.
10
+ *
11
+ * ## Packaging: the composite ships from cedar
12
+ *
13
+ * It hands back an Op and nothing else, so it imports `@intentius/chant/op`
14
+ * only, exactly as the fly lexicon's `flyDeploy` composite does. cedar keeps
15
+ * zero runtime dependency on any hosting lexicon.
16
+ *
17
+ * The cadence is a `schedule` on the Op itself (#2120) — runtime-neutral data
18
+ * the reader decides what to do with, the same field `WatchOp` and
19
+ * `ConvergeOp` set:
20
+ *
21
+ * ```ts
22
+ * export const { op } = PolicyReplayOp({
23
+ * name: "policy-replay",
24
+ * schedule: "0 6 * * *",
25
+ * …
26
+ * });
27
+ * ```
28
+ *
29
+ * `chant operator` ticks it locally, the github/gitlab/forgejo lexicons render
30
+ * it as a CI cron, a hosting lexicon hands it to its own scheduler, and the
31
+ * one-shot executor behind `chant run policy-replay` ignores it.
32
+ * `examples/policy-replay` is the worked recipe.
33
+ *
34
+ * ## Phases
35
+ *
36
+ * ```
37
+ * Artifacts chantBuild — emit policies.dw, the .cedarschema and the
38
+ * .dwschema the replay reads (skippable when they are checked in)
39
+ * Replay dogwoodReplay — run `dogwood replay --format json` over the
40
+ * bundle and the trace, write the divergence report
41
+ * Report dogwoodReplayReport — read that report and act on the mode
42
+ * ```
43
+ *
44
+ * The report file is the seam between the last two phases, the same way
45
+ * `dist/fly.json` is the seam between `build:fly` and `flyApply`: Op steps do
46
+ * not pass return values to one another, so a phase boundary needs an artifact
47
+ * to be a real boundary rather than a cosmetic one.
48
+ */
49
+ import { Op, activity, build, phase } from "@intentius/chant/op";
50
+ /** Where the Replay phase writes its report by default. */
51
+ export const DEFAULT_REPLAY_REPORT_PATH = "dist/dogwood-replay.json";
52
+ /**
53
+ * Replay a policy bundle against a trace — the typed twin of `flyApplyStep`.
54
+ *
55
+ * Resolves to the `dogwoodReplay` activity by name, which `loadActivities`
56
+ * binds when the project lists the `cedar` lexicon. Defaults to the
57
+ * `policyCheck` profile.
58
+ */
59
+ export const dogwoodReplayStep = (opts) => {
60
+ const { profile, ...args } = opts;
61
+ return activity("dogwoodReplay", args, profile ?? "policyCheck");
62
+ };
63
+ /** Read a written replay report and act on its finding mode. */
64
+ export const dogwoodReplayReportStep = (opts = {}) => {
65
+ const { profile, ...args } = opts;
66
+ return activity("dogwoodReplayReport", args, profile ?? "fastIdempotent");
67
+ };
68
+ /**
69
+ * Assemble the replay Op: emit the artifacts, replay them, act on the finding.
70
+ *
71
+ * @example
72
+ * ```typescript
73
+ * export const { op } = PolicyReplayOp({
74
+ * name: "policy-replay",
75
+ * tracePath: "trace/read-after-login.log",
76
+ * expect: [
77
+ * { timestamp: 10, verdict: "allow", determiningRules: [0] },
78
+ * { timestamp: 7200, verdict: "deny", note: "the login is two hours stale" },
79
+ * ],
80
+ * onFinding: "issue",
81
+ * });
82
+ * ```
83
+ */
84
+ export function PolicyReplayOp(config) {
85
+ const path = config.path ?? ".";
86
+ const mode = config.onFinding ?? "report";
87
+ const reportPath = config.reportPath ?? DEFAULT_REPLAY_REPORT_PATH;
88
+ const phases = [];
89
+ if (config.buildScript !== false) {
90
+ phases.push(phase("Artifacts", [build(path, { script: config.buildScript ?? "build" })]));
91
+ }
92
+ phases.push(phase("Replay", [
93
+ {
94
+ ...dogwoodReplayStep({
95
+ cwd: path,
96
+ policiesPath: config.policiesPath ?? "dist/policies.dw",
97
+ policySchemaPath: config.policySchemaPath ?? "dist/app.cedarschema",
98
+ ...(config.eventSchemaPath ? { eventSchemaPath: config.eventSchemaPath } : {}),
99
+ ...(config.macrosPath ? { macrosPath: config.macrosPath } : {}),
100
+ ...(config.providersPath ? { providersPath: config.providersPath } : {}),
101
+ tracePath: config.tracePath,
102
+ ...(config.expect ? { expect: config.expect } : {}),
103
+ mode,
104
+ ...(config.binary ? { binary: config.binary } : {}),
105
+ reportPath,
106
+ }),
107
+ // The divergence count as the run's `Divergences` outcome on the run
108
+ // ledger, so the replays that found something are readable from the
109
+ // ledger rather than out of a log.
110
+ outcomeAttribute: { name: "Divergences", from: "findings" },
111
+ },
112
+ ]), phase("Report", [
113
+ dogwoodReplayReportStep({
114
+ cwd: path,
115
+ reportPath,
116
+ mode,
117
+ ...(config.title ? { title: config.title } : {}),
118
+ ...(config.failOnDivergence ? { failOnDivergence: true } : {}),
119
+ }),
120
+ ]));
121
+ return {
122
+ op: Op({
123
+ name: config.name,
124
+ overview: config.overview ??
125
+ "Replay the declared dogwood policy set against a recorded event trace and report divergence",
126
+ labels: {
127
+ Audit: "true",
128
+ Surface: "cedar-dogwood",
129
+ },
130
+ ...(config.schedule ? { schedule: { cron: config.schedule, overlap: "skip" } } : {}),
131
+ phases,
132
+ }),
133
+ };
134
+ }
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Reading the emitted dogwood artifacts back, for the DWD post-synth checks.
3
+ *
4
+ * The checks judge the *text*, not the in-memory model, for the same reason
5
+ * the CED checks judge `policies.cedar.json`: `chant audit` runs over a
6
+ * checked-in artifact chant did not write, and a wall that only fires on
7
+ * chant's own output is not a wall. It also means the typed builders and the
8
+ * walls are independent — DWDC012 catches a windowless `formerly` even though
9
+ * the builders cannot construct one, because `raw()` and a hand-written `.dw`
10
+ * both can.
11
+ *
12
+ * This is scanning, not parsing. Upstream owns the parser and #1659 owns
13
+ * shelling to it; what is here is the subset of the surface that answers the
14
+ * three questions the walls ask, plus enough comment handling not to be fooled
15
+ * by a commented-out clause.
16
+ */
17
+ import { DEFAULT_MAX_WINDOW, windowSeconds } from "./window.js";
18
+ import { DOGWOOD_EVENT_SCHEMA_SUFFIX, DOGWOOD_POLICY_SUFFIX } from "./serialize.js";
19
+ /** Every `.dw` policy file in the build output. */
20
+ export function dogwoodPolicyFiles(ctx) {
21
+ return filesWithSuffix(ctx, DOGWOOD_POLICY_SUFFIX);
22
+ }
23
+ /** Every `.dwschema` event-schema file in the build output. */
24
+ export function dogwoodSchemaFiles(ctx) {
25
+ return filesWithSuffix(ctx, DOGWOOD_EVENT_SCHEMA_SUFFIX);
26
+ }
27
+ function filesWithSuffix(ctx, suffix) {
28
+ const found = [];
29
+ for (const [lexicon, output] of ctx.outputs) {
30
+ if (typeof output === "string")
31
+ continue;
32
+ for (const [filename, content] of Object.entries(output.files ?? {})) {
33
+ if (typeof content !== "string")
34
+ continue;
35
+ if (!filename.endsWith(suffix))
36
+ continue;
37
+ found.push({ lexicon, source: filename, text: content });
38
+ }
39
+ }
40
+ return found;
41
+ }
42
+ // ── Comments ──────────────────────────────────────────────────────
43
+ /**
44
+ * Blank out `//` comments, leaving string literals intact.
45
+ *
46
+ * String literals stay because a predicate's action id lives inside one
47
+ * (`Drupe::Action::"Read"`), and blanking those would blind every scan below.
48
+ * The walk is string-aware in the other direction too: a `//` inside a quoted
49
+ * value (a URL, say) is not a comment.
50
+ *
51
+ * Comment bytes become spaces so every offset in the result still lines up
52
+ * with the original file.
53
+ */
54
+ export function blankComments(text) {
55
+ const out = text.split("");
56
+ let inString = false;
57
+ for (let i = 0; i < text.length; i++) {
58
+ const ch = text[i];
59
+ if (inString) {
60
+ if (ch === "\\") {
61
+ i++;
62
+ continue;
63
+ }
64
+ if (ch === '"')
65
+ inString = false;
66
+ continue;
67
+ }
68
+ if (ch === '"') {
69
+ inString = true;
70
+ continue;
71
+ }
72
+ if (ch === "/" && text[i + 1] === "/") {
73
+ while (i < text.length && text[i] !== "\n") {
74
+ out[i] = " ";
75
+ i++;
76
+ }
77
+ }
78
+ }
79
+ return out.join("");
80
+ }
81
+ // ── Temporal regions ──────────────────────────────────────────────
82
+ /**
83
+ * The stretches of a `.dw` file that the temporal sub-parser reads: every
84
+ * `temporal { … }` marker body, and every `def temporal … { … }` macro body.
85
+ *
86
+ * Scanning only these is what keeps a Cedar `when { … }` clause from being
87
+ * mistaken for temporal source — `context.retryWindow == 3` should not read as
88
+ * a window, and a Cedar attribute named `since` is not the `since` operator.
89
+ */
90
+ export function temporalRegions(text) {
91
+ const source = blankComments(text);
92
+ const regions = [];
93
+ const markers = /\btemporal\s*\{/g;
94
+ for (let m = markers.exec(source); m !== null; m = markers.exec(source)) {
95
+ const body = matchedBlock(source, m.index + m[0].length - 1);
96
+ if (body !== undefined)
97
+ regions.push(body);
98
+ }
99
+ const macros = /\bdef\s+temporal\s+[A-Za-z_][A-Za-z0-9_]*\s*\([^)]*\)\s*\{/g;
100
+ for (let m = macros.exec(source); m !== null; m = macros.exec(source)) {
101
+ const body = matchedBlock(source, m.index + m[0].length - 1);
102
+ if (body !== undefined)
103
+ regions.push(body);
104
+ }
105
+ return regions;
106
+ }
107
+ /** The text between `text[open]` (a `{`) and its matching `}`, or undefined. */
108
+ function matchedBlock(text, open) {
109
+ let depth = 0;
110
+ let inString = false;
111
+ for (let i = open; i < text.length; i++) {
112
+ const ch = text[i];
113
+ if (inString) {
114
+ if (ch === "\\")
115
+ i++;
116
+ else if (ch === '"')
117
+ inString = false;
118
+ continue;
119
+ }
120
+ if (ch === '"')
121
+ inString = true;
122
+ else if (ch === "{")
123
+ depth++;
124
+ else if (ch === "}") {
125
+ depth--;
126
+ if (depth === 0)
127
+ return text.slice(open + 1, i);
128
+ }
129
+ }
130
+ return undefined;
131
+ }
132
+ const PREDICATE = /([A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z_][A-Za-z0-9_]*)*::"[^"]*")::([A-Za-z_][A-Za-z0-9_]*)/g;
133
+ /** Every temporal predicate head in a `.dw` file, in source order. */
134
+ export function scanPredicates(text) {
135
+ const refs = [];
136
+ for (const region of temporalRegions(text)) {
137
+ for (let m = PREDICATE.exec(region); m !== null; m = PREDICATE.exec(region)) {
138
+ refs.push({ action: m[1], kind: m[2] });
139
+ }
140
+ PREDICATE.lastIndex = 0;
141
+ }
142
+ return refs;
143
+ }
144
+ const WITHIN_WINDOW = /\bwithin\s+(\d+)([smhd])\b/g;
145
+ /** A bare interval in macro-call argument position: `once(1h, …)`, `f(a, 30m)`. */
146
+ const ARGUMENT_WINDOW = /[(,]\s*(\d+)([smhd])\s*(?=[,)])/g;
147
+ /**
148
+ * Every window literal in a `.dw` file's temporal source.
149
+ *
150
+ * Both shapes count against `max_window`: a macro-call interval is the window
151
+ * a `within ?w` in the macro body resolves to, so `once(48h, …)` looks back
152
+ * exactly as far as `formerly within 48h` does.
153
+ */
154
+ export function scanWindows(text) {
155
+ const windows = [];
156
+ for (const region of temporalRegions(text)) {
157
+ for (let m = WITHIN_WINDOW.exec(region); m !== null; m = WITHIN_WINDOW.exec(region)) {
158
+ windows.push(windowRef(m[1], m[2], "within"));
159
+ }
160
+ WITHIN_WINDOW.lastIndex = 0;
161
+ for (let m = ARGUMENT_WINDOW.exec(region); m !== null; m = ARGUMENT_WINDOW.exec(region)) {
162
+ windows.push(windowRef(m[1], m[2], "argument"));
163
+ }
164
+ ARGUMENT_WINDOW.lastIndex = 0;
165
+ }
166
+ return windows;
167
+ }
168
+ function windowRef(value, unit, position) {
169
+ const w = { value: Number(value), unit: unit };
170
+ return { text: `${w.value}${w.unit}`, seconds: windowSeconds(w), position };
171
+ }
172
+ /**
173
+ * Every `formerly` / `previous` / `since` that is not followed by a `within`.
174
+ *
175
+ * Unrepresentable through the builders — {@link formerly} and its siblings all
176
+ * take the window as an argument — but reachable through `raw()`, through a
177
+ * hand-written `.dw`, and through an audit of a file chant never wrote.
178
+ */
179
+ export function scanWindowlessOperators(text) {
180
+ const found = [];
181
+ const pattern = /\b(formerly|previous|since)\b(?!\s+within\b)/g;
182
+ for (const region of temporalRegions(text)) {
183
+ for (let m = pattern.exec(region); m !== null; m = pattern.exec(region)) {
184
+ found.push(m[1]);
185
+ }
186
+ pattern.lastIndex = 0;
187
+ }
188
+ return found;
189
+ }
190
+ const EVENT_DECL = /\bevent\s*<\s*[A-Za-z_][A-Za-z0-9_]*\s*>\s*::\s*([A-Za-z_][A-Za-z0-9_]*)/g;
191
+ const MAX_WINDOW = /\bmax_window\s*=\s*(\d+)([smhd])\b/;
192
+ const PINNED_FIELD = /\bpin\s+[A-Za-z_][A-Za-z0-9_]*\s*:/;
193
+ /** Read a `.dwschema` file's declarations. */
194
+ export function readEventSchema(text) {
195
+ const source = blankComments(text);
196
+ const kinds = [];
197
+ for (let m = EVENT_DECL.exec(source); m !== null; m = EVENT_DECL.exec(source)) {
198
+ if (!kinds.includes(m[1]))
199
+ kinds.push(m[1]);
200
+ }
201
+ EVENT_DECL.lastIndex = 0;
202
+ const max = MAX_WINDOW.exec(source);
203
+ return {
204
+ kinds,
205
+ maxWindowSeconds: max
206
+ ? windowSeconds({ value: Number(max[1]), unit: max[2] })
207
+ : windowSeconds(DEFAULT_MAX_WINDOW),
208
+ maxWindowText: max ? `${max[1]}${max[2]}` : undefined,
209
+ hasPin: PINNED_FIELD.test(source),
210
+ };
211
+ }
212
+ /**
213
+ * The look-back cap in force for a build.
214
+ *
215
+ * With no emitted schema, `ServiceSchema::defaults()` applies and the cap is
216
+ * 24h. With several, the tightest one wins — a window legal under one schema
217
+ * and not another is a window some consumer will reject.
218
+ */
219
+ export function effectiveMaxWindowSeconds(schemas) {
220
+ if (schemas.length === 0) {
221
+ return {
222
+ seconds: windowSeconds(DEFAULT_MAX_WINDOW),
223
+ text: `${DEFAULT_MAX_WINDOW.value}${DEFAULT_MAX_WINDOW.unit}`,
224
+ };
225
+ }
226
+ let tightest = schemas[0];
227
+ for (const s of schemas)
228
+ if (s.maxWindowSeconds < tightest.maxWindowSeconds)
229
+ tightest = s;
230
+ return {
231
+ seconds: tightest.maxWindowSeconds,
232
+ text: tightest.maxWindowText ?? `${DEFAULT_MAX_WINDOW.value}${DEFAULT_MAX_WINDOW.unit}`,
233
+ };
234
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * The `.dw` serializer leg.
3
+ *
4
+ * Runs inside the cedar serializer, not beside it: `serializeDogwood` is
5
+ * called once the cedar policies have been rendered, and its files land in the
6
+ * same `SerializerResult`. That is the whole point of shipping dogwood as a
7
+ * dialect — a `.dw` file stripped of Cedar semantics is meaningless, and a
8
+ * project's temporal policies and its plain Cedar policies are one policy set
9
+ * with two halves.
10
+ *
11
+ * Nothing here re-implements Cedar rendering. The policy head comes from
12
+ * `../serializer`'s `renderPolicyHead`, the scope forms from its
13
+ * `renderScope`, and the `when`/`unless` bodies from its `conditionStrings`,
14
+ * so a change to how a scope is written moves both outputs at once.
15
+ */
16
+ import { isPropertyDeclarable } from "@intentius/chant/declarable";
17
+ import { walkValue } from "@intentius/chant/serializer-walker";
18
+ import { conditionStrings, getProps, isRecord, renderPolicyHead, resolvePolicyId } from "../policy-text.js";
19
+ import { renderCondition } from "./temporal.js";
20
+ import { renderEventSchema } from "./event-schema.js";
21
+ import { renderMacroLibrary } from "./macros.js";
22
+ import { DOGWOOD_EVENT_SCHEMA_FILENAME, DOGWOOD_EVENT_SCHEMA_TYPE, DOGWOOD_MACRO_FILENAME, DOGWOOD_MACRO_LIBRARY_TYPE, DOGWOOD_POLICY_FILENAME, DOGWOOD_POLICY_TYPE, } from "./policy.js";
23
+ /** Files ending in this carry dogwood policy text. */
24
+ export const DOGWOOD_POLICY_SUFFIX = ".dw";
25
+ /** Files ending in this carry an event schema. */
26
+ export const DOGWOOD_EVENT_SCHEMA_SUFFIX = ".dwschema";
27
+ // ── Clause rendering ──────────────────────────────────────────────
28
+ function temporalConditions(value) {
29
+ if (value === undefined || value === null)
30
+ return [];
31
+ const list = Array.isArray(value) ? value : [value];
32
+ return list.filter((node) => isRecord(node) && typeof node.op === "string");
33
+ }
34
+ /**
35
+ * One policy, in `.dw` surface syntax.
36
+ *
37
+ * Clause order is fixed rather than authored: every `when` form, then every
38
+ * `unless` form. Cedar evaluates a policy's conditions as a conjunction, and
39
+ * so does dogwood, so the order carries no meaning — fixing it means a policy
40
+ * that gains a temporal clause does not reshuffle the ones already there.
41
+ */
42
+ export function renderTemporalPolicyText(id, props) {
43
+ const lines = renderPolicyHead(id, props);
44
+ for (const clause of conditionStrings(props.when))
45
+ lines.push(`when { ${clause} }`);
46
+ for (const clause of conditionStrings(props.whenGuardrails))
47
+ lines.push(`when guardrails { ${clause} }`);
48
+ for (const node of temporalConditions(props.whenTemporal)) {
49
+ lines.push("when temporal {", ` ${renderCondition(node)}`, "}");
50
+ }
51
+ for (const clause of conditionStrings(props.unless))
52
+ lines.push(`unless { ${clause} }`);
53
+ for (const clause of conditionStrings(props.unlessGuardrails))
54
+ lines.push(`unless guardrails { ${clause} }`);
55
+ for (const node of temporalConditions(props.unlessTemporal)) {
56
+ lines.push("unless temporal {", ` ${renderCondition(node)}`, "}");
57
+ }
58
+ return lines.join("\n") + ";";
59
+ }
60
+ // ── The walker ────────────────────────────────────────────────────
61
+ /**
62
+ * Same visitor contract as the cedar serializer's: a reference collapses to
63
+ * the text a policy names it by. The temporal AST is plain data, so the walker
64
+ * passes it through untouched while still resolving any Declarable a scope
65
+ * position holds.
66
+ */
67
+ const dogwoodVisitor = {
68
+ attrRef: (logicalName, attribute) => `${logicalName}.${attribute}`,
69
+ resourceRef: (logicalName) => logicalName,
70
+ propertyDeclarable: (entity, walk) => {
71
+ const out = {};
72
+ for (const [key, value] of Object.entries(getProps(entity))) {
73
+ if (key === "entityType" || key === "lexicon")
74
+ continue;
75
+ if (value === undefined)
76
+ continue;
77
+ out[key] = walk(value);
78
+ }
79
+ return out;
80
+ },
81
+ };
82
+ // ── The leg ───────────────────────────────────────────────────────
83
+ /**
84
+ * Render every dogwood entity in the build.
85
+ *
86
+ * Returns `undefined` when the build holds no temporal policies, event schemas
87
+ * or macro libraries at all — a cedar-only project sees no `.dw` output and no
88
+ * behaviour change.
89
+ *
90
+ * Ids come from `resolvePolicyId`, the same rule `cedarPolicyRecords` uses, so
91
+ * a policy's `@id` does not depend on which leg rendered it.
92
+ */
93
+ export function serializeDogwood(entities) {
94
+ // Reverse map first: walkValue resolves a Declarable reference by identity,
95
+ // so every entity has to be known before any of them is walked.
96
+ const entityNames = new Map();
97
+ for (const [name, entity] of entities) {
98
+ entityNames.set(entity, name);
99
+ }
100
+ const policyText = [];
101
+ const inlineMacros = [];
102
+ const macroFiles = new Map();
103
+ const schemaFiles = new Map();
104
+ const warnings = [];
105
+ for (const [name, entity] of entities) {
106
+ if (isPropertyDeclarable(entity))
107
+ continue;
108
+ if (entity.entityType !== DOGWOOD_POLICY_TYPE &&
109
+ entity.entityType !== DOGWOOD_EVENT_SCHEMA_TYPE &&
110
+ entity.entityType !== DOGWOOD_MACRO_LIBRARY_TYPE) {
111
+ continue;
112
+ }
113
+ const props = walkValue(getProps(entity), entityNames, dogwoodVisitor);
114
+ if (entity.entityType === DOGWOOD_POLICY_TYPE) {
115
+ policyText.push(renderTemporalPolicyText(resolvePolicyId(name, props), props));
116
+ continue;
117
+ }
118
+ if (entity.entityType === DOGWOOD_MACRO_LIBRARY_TYPE) {
119
+ const defs = Array.isArray(props.macros) ? props.macros : [];
120
+ if (defs.length === 0) {
121
+ warnings.push(`dogwood: macro library "${name}" declares no macros, so nothing was emitted for it`);
122
+ continue;
123
+ }
124
+ if (props.inline === true) {
125
+ inlineMacros.push(...defs);
126
+ continue;
127
+ }
128
+ const filename = typeof props.filename === "string" ? props.filename : DOGWOOD_MACRO_FILENAME;
129
+ macroFiles.set(filename, [...(macroFiles.get(filename) ?? []), ...defs]);
130
+ continue;
131
+ }
132
+ // DOGWOOD_EVENT_SCHEMA_TYPE
133
+ const schema = props.schema;
134
+ if (!isRecord(schema) || !Array.isArray(schema.events)) {
135
+ warnings.push(`dogwood: event schema "${name}" has no \`schema\` prop, so no .dwschema was written for it`);
136
+ continue;
137
+ }
138
+ const filename = typeof props.filename === "string" ? props.filename : DOGWOOD_EVENT_SCHEMA_FILENAME;
139
+ const existing = schemaFiles.get(filename);
140
+ if (existing) {
141
+ // Two schemas cannot share a file: `max_window` is a single directive at
142
+ // the top, so concatenating them would emit something upstream rejects.
143
+ warnings.push(`dogwood: event schemas "${existing.name}" and "${name}" both target ${filename}; only "${existing.name}" was written — give one of them an explicit \`filename\``);
144
+ continue;
145
+ }
146
+ schemaFiles.set(filename, { name, schema: schema });
147
+ }
148
+ // Warnings alone are enough to report: a dogwood entity that produced no
149
+ // file is exactly the case worth saying out loud, and returning `undefined`
150
+ // here would swallow the reason.
151
+ if (policyText.length === 0 &&
152
+ inlineMacros.length === 0 &&
153
+ macroFiles.size === 0 &&
154
+ schemaFiles.size === 0 &&
155
+ warnings.length === 0) {
156
+ return undefined;
157
+ }
158
+ const files = {};
159
+ if (policyText.length > 0 || inlineMacros.length > 0) {
160
+ const sections = [];
161
+ if (inlineMacros.length > 0)
162
+ sections.push(renderMacroLibrary(inlineMacros).trimEnd());
163
+ sections.push(...policyText);
164
+ files[DOGWOOD_POLICY_FILENAME] = sections.join("\n\n") + "\n";
165
+ }
166
+ for (const [filename, defs] of macroFiles) {
167
+ files[filename] = renderMacroLibrary(defs);
168
+ }
169
+ for (const [filename, { schema }] of schemaFiles) {
170
+ files[filename] = renderEventSchema(schema);
171
+ }
172
+ return { files, warnings };
173
+ }
174
+ /**
175
+ * Every `Dogwood::TemporalPolicy` in a build, with references walked and ids
176
+ * resolved — the temporal counterpart of `cedarPolicyRecords` (#1660).
177
+ *
178
+ * `serializeDogwood` above renders whole *files*; the AgentCore embedding needs
179
+ * the policies one at a time, because `AWS::BedrockAgentCore::Policy` carries a
180
+ * single statement and `EnforcementMode` is set per policy. Both paths walk
181
+ * references through the same visitor, so a scope that names another declared
182
+ * entity resolves identically whichever one rendered it.
183
+ */
184
+ export function dogwoodPolicyRecords(entities) {
185
+ const entityNames = new Map();
186
+ for (const [name, entity] of entities) {
187
+ entityNames.set(entity, name);
188
+ }
189
+ const records = [];
190
+ for (const [name, entity] of entities) {
191
+ if (isPropertyDeclarable(entity))
192
+ continue;
193
+ if (entity.entityType !== DOGWOOD_POLICY_TYPE)
194
+ continue;
195
+ const props = walkValue(getProps(entity), entityNames, dogwoodVisitor);
196
+ records.push({ name, id: resolvePolicyId(name, props), props });
197
+ }
198
+ return records;
199
+ }