@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.
- package/dist/agentcore/embed.js +250 -0
- package/dist/agentcore/enforcement.js +81 -0
- package/dist/agentcore/scan.js +93 -0
- package/dist/avp/ambient.js +100 -0
- package/dist/avp/client.js +318 -0
- package/dist/avp/describe-resources.js +144 -0
- package/dist/avp/embed.js +125 -0
- package/dist/avp/live-export.js +151 -0
- package/dist/avp/ownership.js +144 -0
- package/dist/avp/statement.js +45 -0
- package/dist/avp/store.js +105 -0
- package/dist/avp/testdata/mock-transport.js +100 -0
- package/dist/codegen/docs-cli.js +6 -0
- package/dist/codegen/docs.js +133 -0
- package/dist/codegen/emit.js +331 -0
- package/dist/codegen/generate-cli.js +18 -0
- package/dist/codegen/generate.js +122 -0
- package/dist/codegen/naming.js +90 -0
- package/dist/codegen/package.js +33 -0
- package/dist/commands.js +86 -0
- package/dist/composites/deny-by-default-set.js +51 -0
- package/dist/composites/index.js +9 -0
- package/dist/composites/owner-can-manage.js +48 -0
- package/dist/config.js +159 -0
- package/dist/coverage.js +112 -0
- package/dist/detect.js +61 -0
- package/dist/dogwood/cli.js +477 -0
- package/dist/dogwood/event-schema.js +219 -0
- package/dist/dogwood/index.js +32 -0
- package/dist/dogwood/macros.js +175 -0
- package/dist/dogwood/policy.js +58 -0
- package/dist/dogwood/replay-activity.js +313 -0
- package/dist/dogwood/replay-op.js +134 -0
- package/dist/dogwood/scan.js +234 -0
- package/dist/dogwood/serialize.js +199 -0
- package/dist/dogwood/temporal.js +354 -0
- package/dist/dogwood/trace.js +269 -0
- package/dist/dogwood/upstream.js +40 -0
- package/dist/dogwood/window.js +82 -0
- package/dist/gate-policy-pack.js +189 -0
- package/dist/gate-policy.js +168 -0
- package/dist/gate-schema.js +122 -0
- package/dist/generated/index.js +94 -0
- package/dist/generated/runtime.js +2 -0
- package/dist/import/adapter.js +54 -0
- package/dist/import/clause-text.js +166 -0
- package/dist/import/generator.js +178 -0
- package/dist/import/parser.js +273 -0
- package/dist/index.js +79 -0
- package/dist/init-templates.js +330 -0
- package/dist/integrity.json +2 -2
- package/dist/lint/audit-catalog.js +65 -0
- package/dist/lint/audit-lineage.js +23 -0
- package/dist/lint/post-synth/cedar-helpers.js +123 -0
- package/dist/lint/post-synth/cedc010.js +59 -0
- package/dist/lint/post-synth/cedc011.js +29 -0
- package/dist/lint/post-synth/cedc012.js +34 -0
- package/dist/lint/post-synth/cedc013.js +45 -0
- package/dist/lint/post-synth/cedc014.js +54 -0
- package/dist/lint/post-synth/cede010.js +51 -0
- package/dist/lint/post-synth/cede011.js +36 -0
- package/dist/lint/post-synth/ceds010.js +31 -0
- package/dist/lint/post-synth/ceds011.js +22 -0
- package/dist/lint/post-synth/ceds012.js +31 -0
- package/dist/lint/post-synth/dogwood-helpers.js +94 -0
- package/dist/lint/post-synth/dwdc010.js +38 -0
- package/dist/lint/post-synth/dwdc011.js +34 -0
- package/dist/lint/post-synth/dwdc012.js +24 -0
- package/dist/lint/post-synth/dwdc013.js +29 -0
- package/dist/lint/post-synth/dwde010.js +84 -0
- package/dist/lint/post-synth/dwde011.js +67 -0
- package/dist/lint/post-synth/dwds010.js +21 -0
- package/dist/lint/post-synth/index.js +36 -0
- package/dist/lint/post-synth/wasm-helpers.js +234 -0
- package/dist/lint/rules/index.js +9 -0
- package/dist/lint/rules/policy-shape.js +114 -0
- package/dist/lsp/completions.js +52 -0
- package/dist/lsp/hover.js +56 -0
- package/dist/lsp/registry.d.ts.map +1 -1
- package/dist/lsp/registry.js +51 -0
- package/dist/manifest.json +1 -1
- package/dist/mcp/index.js +82 -0
- package/dist/mcp/policy-coverage.js +134 -0
- package/dist/op/activities/index.js +16 -0
- package/dist/package-cli.js +18 -0
- package/dist/plugin.js +304 -0
- package/dist/policy-text.js +117 -0
- package/dist/schema-artifact.js +66 -0
- package/dist/serializer.js +203 -0
- package/dist/spec/fetch.js +96 -0
- package/dist/spec/parse.js +164 -0
- package/dist/spec/pin.js +170 -0
- package/dist/spec/wasm.js +194 -0
- package/dist/validate-cli.js +7 -0
- package/dist/validate.js +74 -0
- package/package.json +7 -7
- package/src/codegen/package.ts +2 -2
- package/src/coverage.ts +2 -2
- package/src/import/generator.ts +2 -2
- package/src/lsp/registry.ts +2 -1
- 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
|
+
}
|