@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,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dogwood temporal dialect (#1658, epic #1646).
|
|
3
|
+
*
|
|
4
|
+
* A dialect of the cedar lexicon, not a lexicon of its own: `.dw` policies are
|
|
5
|
+
* Cedar policies with temporal clauses, their entity and action schemas are
|
|
6
|
+
* Cedar schemas, and the rendering is shared with the `.cedar` leg. Checks
|
|
7
|
+
* ship under the `DWD` id family, declared on the cedar serializer's
|
|
8
|
+
* `extraRulePrefixes`.
|
|
9
|
+
*
|
|
10
|
+
* Pre-release. Upstream (dogwood-policy/dogwood, Apache-2.0) is a read-only
|
|
11
|
+
* squash-sync mirror with no tags, no releases and no stability statement, and
|
|
12
|
+
* calls itself "NOT intended for production use". The surface here is built
|
|
13
|
+
* against the revision recorded in `./upstream.ts` and re-verified per
|
|
14
|
+
* sub-issue.
|
|
15
|
+
*/
|
|
16
|
+
export { DEFAULT_MAX_WINDOW, isWindowParam, renderWindow, renderWindowValue, window, windowParam, windowSeconds, windowValue, } from "./window.js";
|
|
17
|
+
export { and, arrayOf, binderName, bool, call, compare, count, ctx, decimalOf, entityUid, exists, formerly, int, interval, not, predicate, previous, raw, renderCondition, renderTerm, scopeRef, sigilCondition, since, str, sum, temporalMarker, term, tp, typedBinder, varRef, wildcard, } from "./temporal.js";
|
|
18
|
+
export { DEFAULT_MACRO_NAMES, bind, countDistinctWithin, countWithin, defCedarMacro, defTemporalMacro, defaultMacroLibrary, macroCondition, macroTerm, macroWindow, renderMacroDefinition, renderMacroLibrary, sumWithin, } from "./macros.js";
|
|
19
|
+
export { concrete, declaredEventKinds, defaultEventSchema, eventDeclaration, eventSchema, field, pinContext, pinPrincipal, pinResource, pinnedField, principalType, record, renderEventSchema, resourceType, spreadInputs, spreadOutputs, } from "./event-schema.js";
|
|
20
|
+
export { DOGWOOD_EVENT_SCHEMA_FILENAME, DOGWOOD_EVENT_SCHEMA_TYPE, DOGWOOD_LEXICON, DOGWOOD_MACRO_FILENAME, DOGWOOD_MACRO_LIBRARY_TYPE, DOGWOOD_POLICY_FILENAME, DOGWOOD_POLICY_TYPE, TemporalEventSchema, TemporalMacroLibrary, TemporalPolicy, } from "./policy.js";
|
|
21
|
+
export { DOGWOOD_EVENT_SCHEMA_SUFFIX, DOGWOOD_POLICY_SUFFIX, dogwoodPolicyRecords, renderTemporalPolicyText, serializeDogwood, } from "./serialize.js";
|
|
22
|
+
export { blankComments, dogwoodPolicyFiles, dogwoodSchemaFiles, effectiveMaxWindowSeconds, readEventSchema, scanPredicates, scanWindowlessOperators, scanWindows, temporalRegions, } from "./scan.js";
|
|
23
|
+
export { DOGWOOD_UPSTREAM } from "./upstream.js";
|
|
24
|
+
export { DOGWOOD_BINARY_ENV, DOGWOOD_BINARY_NAME, DOGWOOD_SEARCH_ORDER, configureDogwoodCli, findDogwoodBinary, formatDogwoodDiagnostic, parseLowerOutput, parseReplayOutput, parseValidateOutput, resetDogwoodCli, runDogwoodLower, runDogwoodReplay, runDogwoodValidate, } from "./cli.js";
|
|
25
|
+
// Replay traces (#1661) — typed construction of the §6 line grammar, with
|
|
26
|
+
// both-bag population as the default and the weakening as an opt-out.
|
|
27
|
+
export { auditTrace, decimalValue, entityRef, rawValue, renderTrace, renderTraceFields, renderTraceLine, renderTraceValue, traceEntity, traceEvent, traceFixture, } from "./trace.js";
|
|
28
|
+
// The replay activities (#1661). Contributed by name through
|
|
29
|
+
// `src/op/activities/index.ts`; the helpers are here for direct use.
|
|
30
|
+
export { compareVerdicts, dogwoodReplay, dogwoodReplayReport, renderReplaySummary, resolveReplayInputs, } from "./replay-activity.js";
|
|
31
|
+
// The PolicyReplayOp composite and its typed step builders (#1661).
|
|
32
|
+
export { DEFAULT_REPLAY_REPORT_PATH, PolicyReplayOp, dogwoodReplayReportStep, dogwoodReplayStep, } from "./replay-op.js";
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Macro authoring, and the default library as calls rather than operators.
|
|
3
|
+
*
|
|
4
|
+
* Upstream's policy grammar carries the macro system in the parser, not in the
|
|
5
|
+
* docs:
|
|
6
|
+
*
|
|
7
|
+
* ```
|
|
8
|
+
* def_decl = { kw_def ~ def_kind ~ ident ~ "(" ~ param_list? ~ ")" ~ "{" ~ block_inner ~ "}" ~ ";" }
|
|
9
|
+
* def_kind = { kw_def_cedar | kw_def_temporal }
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* A `cedar` macro's body is a Cedar expression; a `temporal` macro's body is a
|
|
13
|
+
* temporal condition *or* a bare aggregation value (upstream's
|
|
14
|
+
* `temporal_macro_body_entry` tries `agg_expr` first, which is why
|
|
15
|
+
* `count_within` can expand into a `count …` that the call site compares).
|
|
16
|
+
*
|
|
17
|
+
* Two sigils, and they are not interchangeable. `?p` splices the call-site
|
|
18
|
+
* argument literally. `$t` is a fresh binder the macro introduces, gensym'd at
|
|
19
|
+
* every expansion. Both are legal *only* inside a macro body — upstream's
|
|
20
|
+
* well-formedness pass rejects them anywhere else — which is why they are
|
|
21
|
+
* validated here at definition time rather than at render time.
|
|
22
|
+
*
|
|
23
|
+
* The four wrappers at the bottom are the default library
|
|
24
|
+
* (`configuration/default_macros.dw`). They are calls, not operators. A caller
|
|
25
|
+
* who passes `--macros` replaces that library entirely, so a policy built on
|
|
26
|
+
* them is built on an assumption about the other end; {@link defaultMacroLibrary}
|
|
27
|
+
* exists so a project can emit the same definitions itself and stop assuming.
|
|
28
|
+
*/
|
|
29
|
+
import { call, interval, renderCondition, renderTerm, sigilCondition, varRef, } from "./temporal.js";
|
|
30
|
+
import { windowParam } from "./window.js";
|
|
31
|
+
/** `?w` — a value parameter, spliced from the call site. */
|
|
32
|
+
const PARAM = /^\?[A-Za-z_][A-Za-z0-9_]*$/;
|
|
33
|
+
const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
34
|
+
function assertMacroName(name) {
|
|
35
|
+
if (!IDENT.test(name))
|
|
36
|
+
throw new Error(`dogwood: a macro name must be an identifier — got "${name}"`);
|
|
37
|
+
return name;
|
|
38
|
+
}
|
|
39
|
+
function assertParams(params) {
|
|
40
|
+
for (const p of params) {
|
|
41
|
+
if (!PARAM.test(p)) {
|
|
42
|
+
throw new Error(`dogwood: a macro parameter carries the "?" sigil — got "${p}" (write "?${p.replace(/^\?/, "")}")`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return params;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* `def temporal name(?a, ?b) { … };`
|
|
49
|
+
*
|
|
50
|
+
* The body may be a condition or an aggregation term, matching upstream's
|
|
51
|
+
* `temporal_macro_body_entry`; `sum_within` is an aggregation-bodied macro and
|
|
52
|
+
* `once` is a condition-bodied one.
|
|
53
|
+
*/
|
|
54
|
+
export function defTemporalMacro(name, params, body, comment) {
|
|
55
|
+
return {
|
|
56
|
+
kind: "temporal",
|
|
57
|
+
name: assertMacroName(name),
|
|
58
|
+
params: assertParams(params),
|
|
59
|
+
body: isTermBody(body) ? renderTerm(body) : renderCondition(body),
|
|
60
|
+
comment,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function isTermBody(body) {
|
|
64
|
+
return body.op === "term" || body.op === "array" || body.op === "count" || body.op === "sum";
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* `def cedar name(?a) { … };` — the body is Cedar expression text.
|
|
68
|
+
*
|
|
69
|
+
* Cedar's expression grammar is the policy language itself, so it stays a
|
|
70
|
+
* string here for the same reason `Cedar::Policy`'s `when` does.
|
|
71
|
+
*/
|
|
72
|
+
export function defCedarMacro(name, params, body, comment) {
|
|
73
|
+
return { kind: "cedar", name: assertMacroName(name), params: assertParams(params), body, comment };
|
|
74
|
+
}
|
|
75
|
+
// ── The sigils, in the three positions they appear ────────────────
|
|
76
|
+
/**
|
|
77
|
+
* `within ?w` — the window a macro body defers to its call site.
|
|
78
|
+
*
|
|
79
|
+
* The call site supplies a bare interval (`once(1h, …)`), not a `within`
|
|
80
|
+
* clause: the keyword belongs to the operator, so it stays in the body.
|
|
81
|
+
*/
|
|
82
|
+
export function macroWindow(param) {
|
|
83
|
+
return windowParam(param);
|
|
84
|
+
}
|
|
85
|
+
/** `?s` standing for an entire condition, spliced from the call site. */
|
|
86
|
+
export function macroCondition(param) {
|
|
87
|
+
if (!PARAM.test(param))
|
|
88
|
+
throw new Error(`dogwood: a condition parameter carries the "?" sigil — got "${param}"`);
|
|
89
|
+
return sigilCondition(param);
|
|
90
|
+
}
|
|
91
|
+
/** `?a` / `$t` in term or binder position. */
|
|
92
|
+
export function macroTerm(sigil) {
|
|
93
|
+
if (!/^[?$][A-Za-z_][A-Za-z0-9_]*$/.test(sigil)) {
|
|
94
|
+
throw new Error(`dogwood: a macro term carries a "?" or "$" sigil — got "${sigil}"`);
|
|
95
|
+
}
|
|
96
|
+
return varRef(sigil);
|
|
97
|
+
}
|
|
98
|
+
/** One definition as `.dw` source. */
|
|
99
|
+
export function renderMacroDefinition(def) {
|
|
100
|
+
const lines = [];
|
|
101
|
+
if (def.comment) {
|
|
102
|
+
for (const line of def.comment.split("\n"))
|
|
103
|
+
lines.push(`// ${line}`.trimEnd());
|
|
104
|
+
}
|
|
105
|
+
lines.push(`def ${def.kind} ${def.name}(${def.params.join(", ")}) {`);
|
|
106
|
+
lines.push(` ${def.body}`);
|
|
107
|
+
lines.push("};");
|
|
108
|
+
return lines.join("\n");
|
|
109
|
+
}
|
|
110
|
+
/** A whole library — the file `--macros` points at, or a policy set's own prelude. */
|
|
111
|
+
export function renderMacroLibrary(defs) {
|
|
112
|
+
return defs.map(renderMacroDefinition).join("\n\n") + "\n";
|
|
113
|
+
}
|
|
114
|
+
// ── The default library, as calls ─────────────────────────────────
|
|
115
|
+
/** The macro names `configuration/default_macros.dw` ships at the pinned revision. */
|
|
116
|
+
export const DEFAULT_MACRO_NAMES = ["count_within", "sum_within", "count_distinct_within", "bind"];
|
|
117
|
+
/**
|
|
118
|
+
* `count_within(?w, ?s)` — timepoints in the window at which `?s` held.
|
|
119
|
+
*
|
|
120
|
+
* A default-library call, not an operator. See this module's header.
|
|
121
|
+
*/
|
|
122
|
+
export function countWithin(w, condition) {
|
|
123
|
+
return call("count_within", [interval(w), condition]);
|
|
124
|
+
}
|
|
125
|
+
/** `sum_within(?a, ?w, ?body)` — `over` names the sum's bound variable. */
|
|
126
|
+
export function sumWithin(over, w, body) {
|
|
127
|
+
return call("sum_within", [varRef(over), interval(w), body]);
|
|
128
|
+
}
|
|
129
|
+
/** `count_distinct_within(?k, ?w, ?s)` — distinct String keys `k` for which `s` held. */
|
|
130
|
+
export function countDistinctWithin(key, w, condition) {
|
|
131
|
+
return call("count_distinct_within", [varRef(key), interval(w), condition]);
|
|
132
|
+
}
|
|
133
|
+
/** `bind(?n, ?A, ?B)` — names an aggregate result so it can be compared. */
|
|
134
|
+
export function bind(name, aggregate, body) {
|
|
135
|
+
return call("bind", [varRef(name), aggregate, body]);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The default library's four definitions, verbatim from upstream's
|
|
139
|
+
* `dogwood-language/configuration/default_macros.dw` at the pinned revision.
|
|
140
|
+
*
|
|
141
|
+
* Emitting these into a project's own `macros.dw` turns an assumption about
|
|
142
|
+
* the callee's `--macros` into a file the project controls.
|
|
143
|
+
*/
|
|
144
|
+
export function defaultMacroLibrary() {
|
|
145
|
+
return [
|
|
146
|
+
{
|
|
147
|
+
kind: "temporal",
|
|
148
|
+
name: "count_within",
|
|
149
|
+
params: ["?w", "?s"],
|
|
150
|
+
body: "count for ($t: Timepoint). where (formerly within ?w (?s && tp($t)))",
|
|
151
|
+
comment: "Counts the timepoints within window `?w` at which condition `?s` held.",
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
kind: "temporal",
|
|
155
|
+
name: "sum_within",
|
|
156
|
+
params: ["?a", "?w", "?body"],
|
|
157
|
+
body: "sum ?a for (?a: Long), ($t: Timepoint). where (formerly within ?w (?body && tp($t)))",
|
|
158
|
+
comment: "Sums the numeric value `?a` over occurrences of `?body` within window `?w`.",
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
kind: "temporal",
|
|
162
|
+
name: "count_distinct_within",
|
|
163
|
+
params: ["?k", "?w", "?s"],
|
|
164
|
+
body: "count for (?k: String). where (formerly within ?w ?s)",
|
|
165
|
+
comment: "Counts the distinct key values `?k` for which `?s` held within window `?w`.",
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
kind: "temporal",
|
|
169
|
+
name: "bind",
|
|
170
|
+
params: ["?n", "?A", "?B"],
|
|
171
|
+
body: "exists (?n: Long). (?A == ?n && ?B)",
|
|
172
|
+
comment: 'A "let"-style binder: names an aggregate result `?n` and uses it in `?B`.',
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dogwood entity model: a temporal policy, an event schema, a macro library.
|
|
3
|
+
*
|
|
4
|
+
* `Dogwood::TemporalPolicy` is `Cedar::Policy` plus the two clause forms
|
|
5
|
+
* upstream's policy grammar adds. Its head — annotations, effect, the three
|
|
6
|
+
* scope positions — is Cedar's, byte for byte, which is why `./serialize.ts`
|
|
7
|
+
* renders it with the cedar serializer's own `renderPolicyHead` rather than a
|
|
8
|
+
* copy of it. Only the `cond` rule differs:
|
|
9
|
+
*
|
|
10
|
+
* ```
|
|
11
|
+
* cond = { cond_kw ~ (extension_marker | guardrails_tag? ~ "{" ~ expr ~ "}") }
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* — so a clause is `when { … }`, `when guardrails { … }`, or
|
|
15
|
+
* `when temporal { … }`, and the same three under `unless`. `guardrails` is
|
|
16
|
+
* transparent sugar for a bare Cedar expression (upstream discards the tag
|
|
17
|
+
* during lowering); `temporal { … }` is a genuine sub-language dispatched to a
|
|
18
|
+
* different parser.
|
|
19
|
+
*
|
|
20
|
+
* These are not schema-driven the way `Cedar::Policy`'s generated class is.
|
|
21
|
+
* Codegen's input is the project's `.cedarschema`, which says nothing about
|
|
22
|
+
* event kinds or temporal operators, so the dialect's classes are written here
|
|
23
|
+
* and typed against the builders in `./temporal.ts`.
|
|
24
|
+
*/
|
|
25
|
+
import { createResource } from "@intentius/chant/runtime";
|
|
26
|
+
/** The lexicon these entities belong to — dogwood is a dialect of cedar, not a lexicon. */
|
|
27
|
+
export const DOGWOOD_LEXICON = "cedar";
|
|
28
|
+
/** The `entityType` of a temporal policy. */
|
|
29
|
+
export const DOGWOOD_POLICY_TYPE = "Dogwood::TemporalPolicy";
|
|
30
|
+
/** The `entityType` of an event schema. */
|
|
31
|
+
export const DOGWOOD_EVENT_SCHEMA_TYPE = "Dogwood::EventSchema";
|
|
32
|
+
/** The `entityType` of a macro library. */
|
|
33
|
+
export const DOGWOOD_MACRO_LIBRARY_TYPE = "Dogwood::MacroLibrary";
|
|
34
|
+
/** The `.dw` policy set written beside the cedar outputs. */
|
|
35
|
+
export const DOGWOOD_POLICY_FILENAME = "policies.dw";
|
|
36
|
+
/** The `.dwschema` event schema — what `dogwood validate --event-schema` reads. */
|
|
37
|
+
export const DOGWOOD_EVENT_SCHEMA_FILENAME = "events.dwschema";
|
|
38
|
+
/** The macro library — what `dogwood validate --macros` reads. */
|
|
39
|
+
export const DOGWOOD_MACRO_FILENAME = "macros.dw";
|
|
40
|
+
/**
|
|
41
|
+
* A temporal policy. Declare one per policy, the same way as `Cedar::Policy`:
|
|
42
|
+
*
|
|
43
|
+
* ```ts
|
|
44
|
+
* export const readAfterLogin = new TemporalPolicy({
|
|
45
|
+
* action: { eq: 'Drupe::Action::"Read"' },
|
|
46
|
+
* whenTemporal: [
|
|
47
|
+
* formerly("1h", predicate('Drupe::Action::"Login"', "response", {
|
|
48
|
+
* "input.user": ctx("input.user"),
|
|
49
|
+
* })),
|
|
50
|
+
* ],
|
|
51
|
+
* });
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export const TemporalPolicy = createResource(DOGWOOD_POLICY_TYPE, DOGWOOD_LEXICON, {});
|
|
55
|
+
/** The service half of the schema, emitted as `.dwschema` text. */
|
|
56
|
+
export const TemporalEventSchema = createResource(DOGWOOD_EVENT_SCHEMA_TYPE, DOGWOOD_LEXICON, {});
|
|
57
|
+
/** A `def cedar` / `def temporal` library, emitted as `.dw` text. */
|
|
58
|
+
export const TemporalMacroLibrary = createResource(DOGWOOD_MACRO_LIBRARY_TYPE, DOGWOOD_LEXICON, {});
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dogwoodReplay` / `dogwoodReplayReport` — the replay half of PolicyReplayOp
|
|
3
|
+
* (#1661, epic #1646).
|
|
4
|
+
*
|
|
5
|
+
* Contributed the way the fly lexicon contributes `flyApply`: a plain exported
|
|
6
|
+
* async function taking one args object, re-exported from
|
|
7
|
+
* `src/op/activities/index.ts`, resolved **by name** by core's activity
|
|
8
|
+
* registry when a project lists the `cedar` lexicon. It imports no runtime of
|
|
9
|
+
* its own, so the local executor calls it as-is.
|
|
10
|
+
*
|
|
11
|
+
* What it does: takes a policy bundle (inline text or paths), an event trace
|
|
12
|
+
* (typed events, inline text, or a path) and a set of expectations, runs
|
|
13
|
+
* `dogwood replay --format json` through the existing CLI adapter in
|
|
14
|
+
* `./cli.ts`, and returns a typed divergence report — expected versus actual
|
|
15
|
+
* verdict per decision point, with the determining rules and per-evaluation
|
|
16
|
+
* errors carried through.
|
|
17
|
+
*
|
|
18
|
+
* Three contract details from the #1657 verification shape the code:
|
|
19
|
+
*
|
|
20
|
+
* - **Replay exits 0 even when every verdict is DENY.** A nonzero exit means
|
|
21
|
+
* the trace or the policy set failed to load. `./cli.ts` already refuses to
|
|
22
|
+
* read exit codes as verdicts; this module refuses to read a DENY as a
|
|
23
|
+
* failure.
|
|
24
|
+
* - **A run that could not happen is not a run that found nothing.** An
|
|
25
|
+
* unusable invocation or a fatal (a malformed trace line, an unparseable
|
|
26
|
+
* policy set) throws, so the step fails instead of reporting zero
|
|
27
|
+
* divergences.
|
|
28
|
+
* - **`index` is the decision-stream position, not the trace line number.** A
|
|
29
|
+
* history-only event contributes history and no verdict, so expectations
|
|
30
|
+
* written against trace lines would silently address the wrong decision.
|
|
31
|
+
* Expectations therefore match on `timestamp` by default, and on `index`
|
|
32
|
+
* only when the caller says so.
|
|
33
|
+
*
|
|
34
|
+
* The trace input is deliberately generic. An AgentCore session/decision
|
|
35
|
+
* history is the follow-on source (it needs the aws lexicon's activity
|
|
36
|
+
* surface, out of scope here) — a trace is a trace, wherever it came from.
|
|
37
|
+
*/
|
|
38
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
39
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
40
|
+
import { DOGWOOD_SEARCH_ORDER, findDogwoodBinary, formatDogwoodDiagnostic, runDogwoodReplay, } from "./cli.js";
|
|
41
|
+
import { auditTrace, renderTrace } from "./trace.js";
|
|
42
|
+
async function textOf(inline, path, cwd, what) {
|
|
43
|
+
if (inline !== undefined)
|
|
44
|
+
return inline;
|
|
45
|
+
if (path === undefined)
|
|
46
|
+
return undefined;
|
|
47
|
+
const full = isAbsolute(path) ? path : resolve(cwd, path);
|
|
48
|
+
try {
|
|
49
|
+
return await readFile(full, "utf-8");
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
throw new Error(`dogwood replay: could not read ${what} at ${full} — ${err instanceof Error ? err.message : String(err)}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Assemble the bundle and the trace from whichever form the caller supplied.
|
|
57
|
+
*
|
|
58
|
+
* Exported because the composite's Artifacts phase writes files and the Replay
|
|
59
|
+
* phase names them: resolving the same way in a test as in the Op is the point
|
|
60
|
+
* of having one function do it.
|
|
61
|
+
*/
|
|
62
|
+
export async function resolveReplayInputs(args) {
|
|
63
|
+
const cwd = args.cwd ?? process.cwd();
|
|
64
|
+
const policies = await textOf(args.policies, args.policiesPath, cwd, "the .dw policy set");
|
|
65
|
+
if (policies === undefined) {
|
|
66
|
+
throw new Error("dogwood replay: no policy set — pass `policies` or `policiesPath`");
|
|
67
|
+
}
|
|
68
|
+
const policySchema = await textOf(args.policySchema, args.policySchemaPath, cwd, "the Cedar action schema");
|
|
69
|
+
if (policySchema === undefined) {
|
|
70
|
+
throw new Error("dogwood replay: no action schema — `--policy-schema` is not optional to the CLI, so pass `policySchema` or `policySchemaPath`");
|
|
71
|
+
}
|
|
72
|
+
const eventSchema = await textOf(args.eventSchema, args.eventSchemaPath, cwd, "the .dwschema event schema");
|
|
73
|
+
const macros = await textOf(args.macros, args.macrosPath, cwd, "the macro library");
|
|
74
|
+
const providers = await textOf(args.providers, args.providersPath, cwd, "providers.json");
|
|
75
|
+
let trace;
|
|
76
|
+
let traceIssues = [];
|
|
77
|
+
if (args.traceEvents && args.traceEvents.length > 0) {
|
|
78
|
+
// Only the typed form can be audited — there is no trace parser on this
|
|
79
|
+
// side, and guessing one from a regex would report confident nonsense
|
|
80
|
+
// about a format a sync can retune.
|
|
81
|
+
traceIssues = auditTrace(args.traceEvents, {
|
|
82
|
+
...(args.traceDecisionKinds ? { decisionKinds: args.traceDecisionKinds } : {}),
|
|
83
|
+
});
|
|
84
|
+
trace = renderTrace(args.traceEvents);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
trace = await textOf(args.trace, args.tracePath, cwd, "the event trace");
|
|
88
|
+
}
|
|
89
|
+
if (trace === undefined) {
|
|
90
|
+
throw new Error("dogwood replay: no trace — pass `traceEvents`, `trace` or `tracePath`");
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
bundle: {
|
|
94
|
+
policies,
|
|
95
|
+
policySchema,
|
|
96
|
+
...(eventSchema !== undefined ? { eventSchema } : {}),
|
|
97
|
+
...(macros !== undefined ? { macros } : {}),
|
|
98
|
+
...(providers !== undefined ? { providers } : {}),
|
|
99
|
+
},
|
|
100
|
+
trace,
|
|
101
|
+
traceIssues,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Match expectations against the decision stream.
|
|
106
|
+
*
|
|
107
|
+
* Timestamp matching consumes verdicts left to right, so two expectations at
|
|
108
|
+
* the same `@n` address the first and second decision there rather than both
|
|
109
|
+
* addressing the first. A verdict nothing expected is reported too — a policy
|
|
110
|
+
* set that starts deciding somewhere new is drift, and the usual reason a
|
|
111
|
+
* replay is being run at all.
|
|
112
|
+
*/
|
|
113
|
+
export function compareVerdicts(verdicts, expectations) {
|
|
114
|
+
const divergences = [];
|
|
115
|
+
const claimed = new Set();
|
|
116
|
+
for (const expectation of expectations) {
|
|
117
|
+
if (expectation.index === undefined && expectation.timestamp === undefined) {
|
|
118
|
+
throw new Error("dogwood replay: an expectation needs a `timestamp` or an `index`");
|
|
119
|
+
}
|
|
120
|
+
const position = expectation.index !== undefined
|
|
121
|
+
? verdicts.findIndex((v) => v.index === expectation.index)
|
|
122
|
+
: verdicts.findIndex((v, i) => !claimed.has(i) && v.timestamp === expectation.timestamp);
|
|
123
|
+
if (position === -1) {
|
|
124
|
+
const where = expectation.index !== undefined ? `decision index ${expectation.index}` : `@${String(expectation.timestamp)}`;
|
|
125
|
+
divergences.push({
|
|
126
|
+
index: -1,
|
|
127
|
+
timestamp: expectation.timestamp ?? -1,
|
|
128
|
+
expected: expectation.verdict,
|
|
129
|
+
determiningRules: [],
|
|
130
|
+
errors: [],
|
|
131
|
+
detail: `no decision point at ${where} — the trace produced ${verdicts.length} decision(s), and a history-only event produces none`,
|
|
132
|
+
...(expectation.note ? { note: expectation.note } : {}),
|
|
133
|
+
});
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
claimed.add(position);
|
|
137
|
+
const actual = verdicts[position];
|
|
138
|
+
if (actual.verdict !== expectation.verdict) {
|
|
139
|
+
divergences.push({
|
|
140
|
+
index: actual.index,
|
|
141
|
+
timestamp: actual.timestamp,
|
|
142
|
+
expected: expectation.verdict,
|
|
143
|
+
actual: actual.verdict,
|
|
144
|
+
determiningRules: actual.determiningRules,
|
|
145
|
+
errors: actual.errors,
|
|
146
|
+
detail: `expected ${expectation.verdict.toUpperCase()}, replayed ${actual.verdict.toUpperCase()}${actual.determiningRules.length > 0 ? ` (rules: ${actual.determiningRules.join(", ")})` : ""}`,
|
|
147
|
+
...(expectation.note ? { note: expectation.note } : {}),
|
|
148
|
+
});
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (expectation.determiningRules !== undefined) {
|
|
152
|
+
const want = [...expectation.determiningRules].sort((a, b) => a - b).join(", ");
|
|
153
|
+
const got = [...actual.determiningRules].sort((a, b) => a - b).join(", ");
|
|
154
|
+
if (want !== got) {
|
|
155
|
+
divergences.push({
|
|
156
|
+
index: actual.index,
|
|
157
|
+
timestamp: actual.timestamp,
|
|
158
|
+
expected: expectation.verdict,
|
|
159
|
+
actual: actual.verdict,
|
|
160
|
+
determiningRules: actual.determiningRules,
|
|
161
|
+
errors: actual.errors,
|
|
162
|
+
detail: `${actual.verdict.toUpperCase()} as expected, but determined by rules [${got}] rather than [${want}] — the decision is right for a different reason`,
|
|
163
|
+
...(expectation.note ? { note: expectation.note } : {}),
|
|
164
|
+
});
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (actual.errors.length > 0) {
|
|
169
|
+
divergences.push({
|
|
170
|
+
index: actual.index,
|
|
171
|
+
timestamp: actual.timestamp,
|
|
172
|
+
expected: expectation.verdict,
|
|
173
|
+
actual: actual.verdict,
|
|
174
|
+
determiningRules: actual.determiningRules,
|
|
175
|
+
errors: actual.errors,
|
|
176
|
+
detail: `${actual.verdict.toUpperCase()} as expected, but the evaluation errored: ${actual.errors.join("; ")}`,
|
|
177
|
+
...(expectation.note ? { note: expectation.note } : {}),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
verdicts.forEach((verdict, position) => {
|
|
182
|
+
if (claimed.has(position))
|
|
183
|
+
return;
|
|
184
|
+
if (expectations.length === 0)
|
|
185
|
+
return;
|
|
186
|
+
divergences.push({
|
|
187
|
+
index: verdict.index,
|
|
188
|
+
timestamp: verdict.timestamp,
|
|
189
|
+
actual: verdict.verdict,
|
|
190
|
+
determiningRules: verdict.determiningRules,
|
|
191
|
+
errors: verdict.errors,
|
|
192
|
+
detail: `an unexpected decision point: @${verdict.timestamp} replayed ${verdict.verdict.toUpperCase()} and nothing expected a decision there`,
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
// Errors on decision points nobody wrote an expectation for still matter —
|
|
196
|
+
// a provider with no inlined Rhai script errors on every evaluation, and a
|
|
197
|
+
// replay with no expectations at all would otherwise report itself clean.
|
|
198
|
+
if (expectations.length === 0) {
|
|
199
|
+
for (const verdict of verdicts) {
|
|
200
|
+
if (verdict.errors.length === 0)
|
|
201
|
+
continue;
|
|
202
|
+
divergences.push({
|
|
203
|
+
index: verdict.index,
|
|
204
|
+
timestamp: verdict.timestamp,
|
|
205
|
+
actual: verdict.verdict,
|
|
206
|
+
determiningRules: verdict.determiningRules,
|
|
207
|
+
errors: verdict.errors,
|
|
208
|
+
detail: `@${verdict.timestamp} evaluated with errors: ${verdict.errors.join("; ")}`,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return divergences.sort((a, b) => a.timestamp - b.timestamp || a.index - b.index);
|
|
213
|
+
}
|
|
214
|
+
/** The markdown body — printed in `report` mode, posted in the other two. */
|
|
215
|
+
export function renderReplaySummary(report) {
|
|
216
|
+
const lines = ["## Policy replay", ""];
|
|
217
|
+
lines.push(`${report.verdicts.length} decision point(s) replayed; ${report.divergences.length} divergence(s).`, "");
|
|
218
|
+
if (report.divergences.length > 0) {
|
|
219
|
+
lines.push("| @ | index | expected | replayed | detail |", "|---|---|---|---|---|");
|
|
220
|
+
for (const d of report.divergences) {
|
|
221
|
+
lines.push(`| ${d.timestamp === -1 ? "—" : `@${d.timestamp}`} | ${d.index === -1 ? "—" : d.index} | ${d.expected ?? "—"} | ${d.actual ?? "—"} | ${d.detail}${d.note ? ` — ${d.note}` : ""} |`);
|
|
222
|
+
}
|
|
223
|
+
lines.push("");
|
|
224
|
+
}
|
|
225
|
+
if (report.traceIssues.length > 0) {
|
|
226
|
+
lines.push("### Trace weaknesses", "", "These do not fail a replay — they make one report a verdict it did not really test.", "");
|
|
227
|
+
for (const issue of report.traceIssues) {
|
|
228
|
+
lines.push(`- \`${issue.kind}\` @${issue.timestamp}: ${issue.message}`);
|
|
229
|
+
}
|
|
230
|
+
lines.push("");
|
|
231
|
+
}
|
|
232
|
+
if (report.divergences.length === 0 && report.traceIssues.length === 0) {
|
|
233
|
+
lines.push("Every decision point replayed to the verdict it was expected to reach.", "");
|
|
234
|
+
}
|
|
235
|
+
return lines.join("\n");
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Replay a policy bundle against a trace and report divergence.
|
|
239
|
+
*
|
|
240
|
+
* Throws when the CLI could not be used or the run was fatal. That is the
|
|
241
|
+
* distinction `./cli.ts` draws and this preserves: a replay that did not
|
|
242
|
+
* happen must fail the step, never report zero divergences.
|
|
243
|
+
*/
|
|
244
|
+
export async function dogwoodReplay(args) {
|
|
245
|
+
const cwd = args.cwd ?? process.cwd();
|
|
246
|
+
const mode = args.mode ?? "report";
|
|
247
|
+
const binary = args.binary ?? findDogwoodBinary(cwd)?.path;
|
|
248
|
+
if (!binary) {
|
|
249
|
+
throw new Error(`dogwood replay: no \`dogwood\` binary. chant looked at ${DOGWOOD_SEARCH_ORDER}. Upstream ships only a Rust CLI — build it from dogwood-policy/dogwood and point chant at it.`);
|
|
250
|
+
}
|
|
251
|
+
const { bundle, trace, traceIssues } = await resolveReplayInputs(args);
|
|
252
|
+
const result = runDogwoodReplay(binary, bundle, trace);
|
|
253
|
+
if (result.kind === "unusable") {
|
|
254
|
+
throw new Error(`dogwood replay: ${result.reason}`);
|
|
255
|
+
}
|
|
256
|
+
if (result.kind === "fatal") {
|
|
257
|
+
const related = result.related.map((d) => `\n ${formatDogwoodDiagnostic(d)}`).join("");
|
|
258
|
+
throw new Error(`dogwood replay: ${formatDogwoodDiagnostic(result.error)}${related}`);
|
|
259
|
+
}
|
|
260
|
+
const divergences = compareVerdicts(result.verdicts, args.expect ?? []);
|
|
261
|
+
const partial = {
|
|
262
|
+
ok: divergences.length === 0,
|
|
263
|
+
mode,
|
|
264
|
+
verdicts: result.verdicts,
|
|
265
|
+
divergences,
|
|
266
|
+
findings: divergences.length,
|
|
267
|
+
traceIssues,
|
|
268
|
+
};
|
|
269
|
+
const report = { ...partial, summary: renderReplaySummary(partial) };
|
|
270
|
+
if (args.reportPath) {
|
|
271
|
+
const full = isAbsolute(args.reportPath) ? args.reportPath : resolve(cwd, args.reportPath);
|
|
272
|
+
await mkdir(dirname(full), { recursive: true });
|
|
273
|
+
await writeFile(full, JSON.stringify(report, null, 2) + "\n", "utf-8");
|
|
274
|
+
}
|
|
275
|
+
return report;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Turn a written replay report into the thing the finding mode calls for.
|
|
279
|
+
*
|
|
280
|
+
* It renders and returns; it does not open anything. Same as
|
|
281
|
+
* `workflowSupplyChainAudit`, and for the same reason — the cedar lexicon has
|
|
282
|
+
* no forge client and should not grow one to reach GitHub, GitLab or Forgejo.
|
|
283
|
+
* `report` mode prints the body; `issue` and `pull-request` hand back the
|
|
284
|
+
* title and body for whatever step opens them.
|
|
285
|
+
*/
|
|
286
|
+
export async function dogwoodReplayReport(args) {
|
|
287
|
+
const cwd = args.cwd ?? process.cwd();
|
|
288
|
+
let report = args.report;
|
|
289
|
+
if (!report) {
|
|
290
|
+
if (!args.reportPath) {
|
|
291
|
+
throw new Error("dogwood replay report: pass `report` or `reportPath`");
|
|
292
|
+
}
|
|
293
|
+
const full = isAbsolute(args.reportPath) ? args.reportPath : resolve(cwd, args.reportPath);
|
|
294
|
+
try {
|
|
295
|
+
report = JSON.parse(await readFile(full, "utf-8"));
|
|
296
|
+
}
|
|
297
|
+
catch (err) {
|
|
298
|
+
throw new Error(`dogwood replay report: could not read the replay report at ${full} — ${err instanceof Error ? err.message : String(err)}. The Replay phase writes it; a Report phase that runs without one has nothing to report on.`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const mode = args.mode ?? report.mode;
|
|
302
|
+
const findings = report.findings;
|
|
303
|
+
const actionable = findings > 0 || report.traceIssues.length > 0;
|
|
304
|
+
const title = args.title ?? `Policy replay: ${findings} divergence(s)`;
|
|
305
|
+
if (mode === "report") {
|
|
306
|
+
// eslint-disable-next-line no-console -- report mode's whole output is this.
|
|
307
|
+
console.log(report.summary);
|
|
308
|
+
}
|
|
309
|
+
if (args.failOnDivergence && findings > 0) {
|
|
310
|
+
throw new Error(`dogwood replay: ${findings} divergence(s) between the declared policy set and the replayed trace`);
|
|
311
|
+
}
|
|
312
|
+
return { mode, findings, title, body: report.summary, actionable };
|
|
313
|
+
}
|