@intentius/chant-lexicon-cedar 0.44.8 → 0.44.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -2
- package/dist/agentcore/embed.d.ts +189 -0
- package/dist/agentcore/embed.d.ts.map +1 -0
- package/dist/agentcore/enforcement.d.ts +76 -0
- package/dist/agentcore/enforcement.d.ts.map +1 -0
- package/dist/agentcore/scan.d.ts +46 -0
- package/dist/agentcore/scan.d.ts.map +1 -0
- package/dist/codegen/docs-dogwood.d.ts +21 -0
- package/dist/codegen/docs-dogwood.d.ts.map +1 -0
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/codegen/package.d.ts.map +1 -1
- package/dist/config.d.ts +25 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/dogwood/cli.d.ts +247 -0
- package/dist/dogwood/cli.d.ts.map +1 -0
- package/dist/dogwood/event-schema.d.ts +161 -0
- package/dist/dogwood/event-schema.d.ts.map +1 -0
- package/dist/dogwood/index.d.ts +39 -0
- package/dist/dogwood/index.d.ts.map +1 -0
- package/dist/dogwood/macros.d.ts +96 -0
- package/dist/dogwood/macros.d.ts.map +1 -0
- package/dist/dogwood/policy.d.ts +120 -0
- package/dist/dogwood/policy.d.ts.map +1 -0
- package/dist/dogwood/replay-activity.d.ts +196 -0
- package/dist/dogwood/replay-activity.d.ts.map +1 -0
- package/dist/dogwood/replay-op.d.ts +165 -0
- package/dist/dogwood/replay-op.d.ts.map +1 -0
- package/dist/dogwood/scan.d.ts +109 -0
- package/dist/dogwood/scan.d.ts.map +1 -0
- package/dist/dogwood/serialize.d.ts +66 -0
- package/dist/dogwood/serialize.d.ts.map +1 -0
- package/dist/dogwood/temporal.d.ts +259 -0
- package/dist/dogwood/temporal.d.ts.map +1 -0
- package/dist/dogwood/trace.d.ts +215 -0
- package/dist/dogwood/trace.d.ts.map +1 -0
- package/dist/dogwood/upstream.d.ts +41 -0
- package/dist/dogwood/upstream.d.ts.map +1 -0
- package/dist/dogwood/window.d.ts +73 -0
- package/dist/dogwood/window.d.ts.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +15 -3
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/dogwood-helpers.d.ts +63 -0
- package/dist/lint/post-synth/dogwood-helpers.d.ts.map +1 -0
- package/dist/lint/post-synth/dwdc010.d.ts +25 -0
- package/dist/lint/post-synth/dwdc010.d.ts.map +1 -0
- package/dist/lint/post-synth/dwdc011.d.ts +19 -0
- package/dist/lint/post-synth/dwdc011.d.ts.map +1 -0
- package/dist/lint/post-synth/dwdc012.d.ts +21 -0
- package/dist/lint/post-synth/dwdc012.d.ts.map +1 -0
- package/dist/lint/post-synth/dwdc013.d.ts +33 -0
- package/dist/lint/post-synth/dwdc013.d.ts.map +1 -0
- package/dist/lint/post-synth/dwde010.d.ts +32 -0
- package/dist/lint/post-synth/dwde010.d.ts.map +1 -0
- package/dist/lint/post-synth/dwde011.d.ts +33 -0
- package/dist/lint/post-synth/dwde011.d.ts.map +1 -0
- package/dist/lint/post-synth/dwds010.d.ts +24 -0
- package/dist/lint/post-synth/dwds010.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +7 -0
- package/dist/okf/rules/DWDC010.md +11 -0
- package/dist/okf/rules/DWDC011.md +11 -0
- package/dist/okf/rules/DWDC012.md +11 -0
- package/dist/okf/rules/DWDC013.md +15 -0
- package/dist/okf/rules/DWDE010.md +11 -0
- package/dist/okf/rules/DWDE011.md +11 -0
- package/dist/okf/rules/DWDS010.md +11 -0
- package/dist/okf/types/Policy.md +1 -0
- package/dist/op/activities/index.d.ts +18 -0
- package/dist/op/activities/index.d.ts.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/policy-text.d.ts +53 -0
- package/dist/policy-text.d.ts.map +1 -0
- package/dist/rules/dogwood-helpers.ts +139 -0
- package/dist/rules/dwdc010.ts +62 -0
- package/dist/rules/dwdc011.ts +61 -0
- package/dist/rules/dwdc012.ts +46 -0
- package/dist/rules/dwdc013.ts +64 -0
- package/dist/rules/dwde010.ts +130 -0
- package/dist/rules/dwde011.ts +108 -0
- package/dist/rules/dwds010.ts +46 -0
- package/dist/serializer.d.ts +10 -18
- package/dist/serializer.d.ts.map +1 -1
- package/dist/skills/chant-cedar-authoring.md +180 -0
- package/dist/skills/chant-cedar-avp-embedding.md +125 -0
- package/dist/skills/chant-cedar-dogwood.md +327 -0
- package/dist/skills/chant-cedar-meta-policy.md +119 -0
- package/package.json +7 -2
- package/src/agentcore/embed.test.ts +254 -0
- package/src/agentcore/embed.ts +399 -0
- package/src/agentcore/enforcement.test.ts +43 -0
- package/src/agentcore/enforcement.ts +92 -0
- package/src/agentcore/scan.ts +119 -0
- package/src/codegen/docs-dogwood.ts +1119 -0
- package/src/codegen/docs.ts +66 -1
- package/src/codegen/package.ts +3 -2
- package/src/config.test.ts +12 -0
- package/src/config.ts +28 -0
- package/src/dogwood/cli.test.ts +513 -0
- package/src/dogwood/cli.ts +666 -0
- package/src/dogwood/event-schema.test.ts +218 -0
- package/src/dogwood/event-schema.ts +318 -0
- package/src/dogwood/index.ts +271 -0
- package/src/dogwood/macros.test.ts +104 -0
- package/src/dogwood/macros.ts +229 -0
- package/src/dogwood/policy.test.ts +94 -0
- package/src/dogwood/policy.ts +141 -0
- package/src/dogwood/replay-activity.test.ts +481 -0
- package/src/dogwood/replay-activity.ts +506 -0
- package/src/dogwood/replay-op.ts +242 -0
- package/src/dogwood/scan.ts +287 -0
- package/src/dogwood/serialize.test.ts +331 -0
- package/src/dogwood/serialize.ts +246 -0
- package/src/dogwood/temporal.test.ts +272 -0
- package/src/dogwood/temporal.ts +592 -0
- package/src/dogwood/testdata/custom-kinds.dwschema +17 -0
- package/src/dogwood/testdata/default-macros.dw +23 -0
- package/src/dogwood/testdata/lowered-read-after-login.json +13 -0
- package/src/dogwood/testdata/max-window-raised.dwschema +25 -0
- package/src/dogwood/testdata/pinned.dwschema +31 -0
- package/src/dogwood/testdata/read-after-login.cedarschema +20 -0
- package/src/dogwood/testdata/read-after-login.dw +17 -0
- package/src/dogwood/testdata/temporal-policies.dw +53 -0
- package/src/dogwood/trace.test.ts +231 -0
- package/src/dogwood/trace.ts +471 -0
- package/src/dogwood/upstream.ts +41 -0
- package/src/dogwood/window.ts +124 -0
- package/src/index.ts +76 -0
- package/src/lint/audit-catalog.ts +64 -0
- package/src/lint/post-synth/dogwood-helpers.ts +139 -0
- package/src/lint/post-synth/dwd-post-synth.test.ts +374 -0
- package/src/lint/post-synth/dwdc010.ts +62 -0
- package/src/lint/post-synth/dwdc011.ts +61 -0
- package/src/lint/post-synth/dwdc012.ts +46 -0
- package/src/lint/post-synth/dwdc013.ts +64 -0
- package/src/lint/post-synth/dwde-post-synth.test.ts +368 -0
- package/src/lint/post-synth/dwde010.ts +130 -0
- package/src/lint/post-synth/dwde011.ts +108 -0
- package/src/lint/post-synth/dwds010.ts +46 -0
- package/src/lint/post-synth/index.ts +14 -0
- package/src/lint/post-synth/post-synth.test.ts +7 -3
- package/src/op/activities/index.ts +27 -0
- package/src/plugin.test.ts +3 -2
- package/src/plugin.ts +30 -0
- package/src/policy-text.ts +128 -0
- package/src/serializer.ts +71 -109
- package/src/skills/chant-cedar-dogwood.md +327 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DWDE010: the emitted `.dw` set validates clean under `dogwood validate`
|
|
3
|
+
*
|
|
4
|
+
* The CLI-gated half of the epic's validation split. Everything the DWDC walls
|
|
5
|
+
* ask is answerable in TypeScript and gates unconditionally; everything else —
|
|
6
|
+
* macro expansion, the temporal type checker, the Cedar body checked against
|
|
7
|
+
* the action schema *through upstream's own frontend* — needs upstream's Rust
|
|
8
|
+
* frontend, which ships as a binary and nothing else. No npm package, no wasm
|
|
9
|
+
* build, no bindings.
|
|
10
|
+
*
|
|
11
|
+
* So this check has two modes and says which one it is in:
|
|
12
|
+
*
|
|
13
|
+
* - **Binary present.** Runs `dogwood validate --format json` over each
|
|
14
|
+
* emitted policy set and reports every finding as an error. Byte-offset
|
|
15
|
+
* labels come through as byte offsets — see `../../dogwood/cli.ts` for why
|
|
16
|
+
* they are not converted to line/column.
|
|
17
|
+
* - **Binary absent.** Exactly one `info` finding naming the binary, where
|
|
18
|
+
* chant looked, and this issue. The epic's words are "an explicit,
|
|
19
|
+
* issue-linked exception, not a silent one" — a check that quietly passes
|
|
20
|
+
* when it could not run is claiming a guarantee it did not make, which is
|
|
21
|
+
* the same failure CEDE010's no-schema advisory exists to avoid.
|
|
22
|
+
*
|
|
23
|
+
* A run that could not be made at all — a spawn failure, an unknown flag, JSON
|
|
24
|
+
* in a shape this adapter does not know — is `warning`, not `error`. Upstream
|
|
25
|
+
* is a read-only squash-sync mirror with no tags and no changelog, and a flag
|
|
26
|
+
* rename in a sync would otherwise fail every build that has the binary
|
|
27
|
+
* installed. The adapter never reads exit 2 as "rejected" on its own for the
|
|
28
|
+
* same reason: clap spends that code on usage errors too.
|
|
29
|
+
*/
|
|
30
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
31
|
+
import {
|
|
32
|
+
DOGWOOD_BINARY_NAME,
|
|
33
|
+
DOGWOOD_SEARCH_ORDER,
|
|
34
|
+
findDogwoodBinary,
|
|
35
|
+
formatDogwoodDiagnostic,
|
|
36
|
+
runDogwoodValidate,
|
|
37
|
+
} from "../../dogwood/cli";
|
|
38
|
+
import { describeBundle, planDogwoodRuns } from "./dogwood-helpers";
|
|
39
|
+
|
|
40
|
+
export const dwde010: PostSynthCheck = {
|
|
41
|
+
id: "DWDE010",
|
|
42
|
+
description: "Emitted dogwood policy sets validate clean under `dogwood validate`, when the binary is available",
|
|
43
|
+
|
|
44
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
45
|
+
const plan = planDogwoodRuns(ctx);
|
|
46
|
+
if (!plan.hasPolicies) return [];
|
|
47
|
+
|
|
48
|
+
const binary = findDogwoodBinary();
|
|
49
|
+
if (!binary) {
|
|
50
|
+
return [
|
|
51
|
+
{
|
|
52
|
+
checkId: "DWDE010",
|
|
53
|
+
severity: "info",
|
|
54
|
+
message: `This build emitted dogwood .dw policies, but no \`${DOGWOOD_BINARY_NAME}\` binary was found, so full .dw validation did not run — the DWDC walls checked what TypeScript can answer and nothing checked macro expansion, the temporal type check, or the Cedar body against the action schema. chant looked at ${DOGWOOD_SEARCH_ORDER}. See chant #1659.`,
|
|
55
|
+
lexicon: plan.lexicon,
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (plan.blocked) {
|
|
61
|
+
return [
|
|
62
|
+
{
|
|
63
|
+
checkId: "DWDE010",
|
|
64
|
+
severity: "info",
|
|
65
|
+
message: `This build emitted dogwood .dw policies and \`${DOGWOOD_BINARY_NAME}\` is available, but ${plan.blocked}, so full .dw validation did not run. See chant #1659.`,
|
|
66
|
+
lexicon: plan.lexicon,
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
72
|
+
|
|
73
|
+
for (const prepared of plan.bundles) {
|
|
74
|
+
const where = describeBundle(prepared);
|
|
75
|
+
const result = runDogwoodValidate(binary.path, prepared.bundle);
|
|
76
|
+
|
|
77
|
+
if (result.kind === "unusable") {
|
|
78
|
+
diagnostics.push({
|
|
79
|
+
checkId: "DWDE010",
|
|
80
|
+
severity: "warning",
|
|
81
|
+
message: `Dogwood policy set ${where} could not be validated — ${result.reason}. The policy set was neither accepted nor rejected.`,
|
|
82
|
+
entity: prepared.source,
|
|
83
|
+
lexicon: prepared.lexicon,
|
|
84
|
+
});
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (result.kind === "fatal") {
|
|
89
|
+
for (const finding of [result.error, ...result.related]) {
|
|
90
|
+
diagnostics.push({
|
|
91
|
+
checkId: "DWDE010",
|
|
92
|
+
severity: "error",
|
|
93
|
+
message: `Dogwood policy set ${where} could not be parsed or lowered: ${formatDogwoodDiagnostic(finding)}`,
|
|
94
|
+
entity: prepared.source,
|
|
95
|
+
lexicon: prepared.lexicon,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (result.kind === "rejected") {
|
|
102
|
+
for (const finding of result.errors) {
|
|
103
|
+
diagnostics.push({
|
|
104
|
+
checkId: "DWDE010",
|
|
105
|
+
severity: "error",
|
|
106
|
+
message: `Dogwood policy set ${where} fails \`dogwood validate\`: ${formatDogwoodDiagnostic(finding)}`,
|
|
107
|
+
entity: prepared.source,
|
|
108
|
+
lexicon: prepared.lexicon,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Upstream's own warnings ride along at warning severity. They do not
|
|
114
|
+
// fail `dogwood validate` (`passed` ignores them), and they should not
|
|
115
|
+
// fail a build either — but dropping them would throw away the half of
|
|
116
|
+
// the report that says a policy is legal and pointless.
|
|
117
|
+
for (const finding of result.warnings) {
|
|
118
|
+
diagnostics.push({
|
|
119
|
+
checkId: "DWDE010",
|
|
120
|
+
severity: "warning",
|
|
121
|
+
message: `Dogwood policy set ${where} draws a \`dogwood validate\` warning: ${formatDogwoodDiagnostic(finding)}`,
|
|
122
|
+
entity: prepared.source,
|
|
123
|
+
lexicon: prepared.lexicon,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return diagnostics;
|
|
129
|
+
},
|
|
130
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DWDE011: the lowered Cedar body validates clean under Cedar's own validator
|
|
3
|
+
*
|
|
4
|
+
* The epic's stated design for the non-temporal half: chant does not
|
|
5
|
+
* re-implement lowering — `dogwood lower` produces the analyzable plain-Cedar
|
|
6
|
+
* form, and a reimplementation would drift — but once that form exists, Cedar's
|
|
7
|
+
* own validator is the thing that judges it. The #1657 verification put all 86
|
|
8
|
+
* upstream example bundles through this exact path and every one parsed and
|
|
9
|
+
* validated clean in strict mode, which is what makes a finding here mean
|
|
10
|
+
* something: the lowered output is genuinely plain Cedar, so a validation error
|
|
11
|
+
* is the policy's, not the pipeline's.
|
|
12
|
+
*
|
|
13
|
+
* What this catches that DWDE010 does not is narrow but real. `dogwood
|
|
14
|
+
* validate` type-checks through upstream's vendored Cedar; this runs the
|
|
15
|
+
* *published* `@cedar-policy/cedar-wasm` over the same artifacts. A body that
|
|
16
|
+
* upstream's pinned Cedar accepts and the Cedar the rest of chant validates
|
|
17
|
+
* against rejects is exactly the drift the pin exists to make visible — and the
|
|
18
|
+
* augmented schema, with its hoisted `context.*` temporal slots, is the form a
|
|
19
|
+
* downstream Cedar policy store will actually receive.
|
|
20
|
+
*
|
|
21
|
+
* The cedar-wasm traps from #1648 all apply and are handled in
|
|
22
|
+
* `./wasm-helpers.ts`: `type: "success"` means validation *ran*, not that it
|
|
23
|
+
* passed; `validationErrors` comes back in a different order almost every call
|
|
24
|
+
* and is sorted before anything reads it; a malformed call throws rather than
|
|
25
|
+
* returning a failure answer.
|
|
26
|
+
*
|
|
27
|
+
* Silent when `lower` did not run — the binary is absent, or no action schema
|
|
28
|
+
* was emitted. Both cases are DWDE010's advisory to report, once, rather than
|
|
29
|
+
* twice in different words.
|
|
30
|
+
*/
|
|
31
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
32
|
+
import { findDogwoodBinary, runDogwoodLower } from "../../dogwood/cli";
|
|
33
|
+
import { describeBundle, planDogwoodRuns } from "./dogwood-helpers";
|
|
34
|
+
import { loadWasm, validatePolicySet } from "./wasm-helpers";
|
|
35
|
+
|
|
36
|
+
export const dwde011: PostSynthCheck = {
|
|
37
|
+
id: "DWDE011",
|
|
38
|
+
description: "The Cedar `dogwood lower` produces validates clean against the augmented schema (cedar-wasm validate)",
|
|
39
|
+
|
|
40
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
41
|
+
const plan = planDogwoodRuns(ctx);
|
|
42
|
+
if (!plan.hasPolicies || plan.blocked || plan.bundles.length === 0) return [];
|
|
43
|
+
|
|
44
|
+
const binary = findDogwoodBinary();
|
|
45
|
+
if (!binary) return [];
|
|
46
|
+
|
|
47
|
+
// A missing validator is not this check's finding to report; the cedar leg
|
|
48
|
+
// says so already through CEDC010.
|
|
49
|
+
const wasm = loadWasm();
|
|
50
|
+
if (!wasm) return [];
|
|
51
|
+
|
|
52
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
53
|
+
|
|
54
|
+
for (const prepared of plan.bundles) {
|
|
55
|
+
const where = describeBundle(prepared);
|
|
56
|
+
const lowered = runDogwoodLower(binary.path, prepared.bundle);
|
|
57
|
+
|
|
58
|
+
if (lowered.kind === "unusable") {
|
|
59
|
+
diagnostics.push({
|
|
60
|
+
checkId: "DWDE011",
|
|
61
|
+
severity: "warning",
|
|
62
|
+
message: `Dogwood policy set ${where} could not be lowered to Cedar — ${lowered.reason}. Its non-temporal body was not checked by Cedar's validator.`,
|
|
63
|
+
entity: prepared.source,
|
|
64
|
+
lexicon: prepared.lexicon,
|
|
65
|
+
});
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (lowered.kind === "fatal") {
|
|
70
|
+
// The same fatal DWDE010 reports from its own run. Saying it twice
|
|
71
|
+
// helps nobody, so this arm stays quiet and lets the validate leg own
|
|
72
|
+
// the parse/lower channel.
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const { cedarPolicies, cedarSchema } = lowered.value;
|
|
77
|
+
|
|
78
|
+
// The lowered policies go in as Cedar text. cedar-wasm synthesizes ids
|
|
79
|
+
// (`policy0`, `policy1`) for a bare-string set and does not read `@id`
|
|
80
|
+
// annotations as ids (#1648), so a finding names the .dw file it came
|
|
81
|
+
// from as well as the id the validator used.
|
|
82
|
+
const outcome = validatePolicySet(wasm, { staticPolicies: cedarPolicies }, cedarSchema);
|
|
83
|
+
|
|
84
|
+
if (outcome.failure) {
|
|
85
|
+
diagnostics.push({
|
|
86
|
+
checkId: "DWDE011",
|
|
87
|
+
severity: "error",
|
|
88
|
+
message: `The Cedar lowered from dogwood policy set ${where} could not be validated: ${outcome.failure}`,
|
|
89
|
+
entity: prepared.source,
|
|
90
|
+
lexicon: prepared.lexicon,
|
|
91
|
+
});
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const finding of outcome.errors) {
|
|
96
|
+
diagnostics.push({
|
|
97
|
+
checkId: "DWDE011",
|
|
98
|
+
severity: "error",
|
|
99
|
+
message: `The Cedar lowered from dogwood policy set ${where} fails Cedar validation (lowered policy "${finding.policyId}"): ${finding.message}`,
|
|
100
|
+
entity: prepared.source,
|
|
101
|
+
lexicon: prepared.lexicon,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return diagnostics;
|
|
107
|
+
},
|
|
108
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DWDS010: an emitted event schema with no pinned field widens every predicate
|
|
3
|
+
*
|
|
4
|
+
* The #1657 verification is specific about this one. `ServiceSchema::defaults()`
|
|
5
|
+
* — what runs when nobody passes `--event-schema` — uses
|
|
6
|
+
* `configuration/event-schemas/pinned.dwschema`, which carries
|
|
7
|
+
* `pin callerPrincipal: principalType(A) = principal` on every kind. Every
|
|
8
|
+
* temporal predicate is therefore correlated to the deciding request's
|
|
9
|
+
* principal, and events from other principals are invisible.
|
|
10
|
+
*
|
|
11
|
+
* Supplying any event schema opts out of that default wholesale. So a schema
|
|
12
|
+
* emitted without a pin does not merely "not add" a correlation: it removes
|
|
13
|
+
* one the policy author very likely assumed, and every `formerly` in the set
|
|
14
|
+
* starts matching other principals' events. That is a legitimate design —
|
|
15
|
+
* cross-principal correlation is the reason to write your own schema — but it
|
|
16
|
+
* is a decision, and a decision nobody can see in a diff is one nobody made.
|
|
17
|
+
*
|
|
18
|
+
* Report-only: chant does not know which the author wanted, only that it
|
|
19
|
+
* should be said out loud. `defaultEventSchema({ pinCallerPrincipal: false })`
|
|
20
|
+
* also stamps the reasoning into the emitted file.
|
|
21
|
+
*/
|
|
22
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
23
|
+
import { dogwoodSchemaFiles, readEventSchema } from "../../dogwood/scan";
|
|
24
|
+
|
|
25
|
+
export const dwds010: PostSynthCheck = {
|
|
26
|
+
id: "DWDS010",
|
|
27
|
+
description: "An emitted dogwood event schema pins its temporal predicates to a request-side value",
|
|
28
|
+
|
|
29
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
30
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
31
|
+
|
|
32
|
+
for (const schema of dogwoodSchemaFiles(ctx)) {
|
|
33
|
+
const facts = readEventSchema(schema.text);
|
|
34
|
+
if (facts.kinds.length === 0 || facts.hasPin) continue;
|
|
35
|
+
diagnostics.push({
|
|
36
|
+
checkId: "DWDS010",
|
|
37
|
+
severity: "warning",
|
|
38
|
+
message: `Dogwood event schema "${schema.source}" declares no pinned field, so temporal predicates correlate across every principal — wider than upstream's default, which pins callerPrincipal to the deciding request's principal. Add a pinned field, or record why cross-principal correlation is wanted.`,
|
|
39
|
+
entity: schema.source,
|
|
40
|
+
lexicon: schema.lexicon,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return diagnostics;
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -10,6 +10,13 @@ import { cede011 } from "./cede011";
|
|
|
10
10
|
import { ceds010 } from "./ceds010";
|
|
11
11
|
import { ceds011 } from "./ceds011";
|
|
12
12
|
import { ceds012 } from "./ceds012";
|
|
13
|
+
import { dwdc010 } from "./dwdc010";
|
|
14
|
+
import { dwdc011 } from "./dwdc011";
|
|
15
|
+
import { dwdc012 } from "./dwdc012";
|
|
16
|
+
import { dwdc013 } from "./dwdc013";
|
|
17
|
+
import { dwde010 } from "./dwde010";
|
|
18
|
+
import { dwde011 } from "./dwde011";
|
|
19
|
+
import { dwds010 } from "./dwds010";
|
|
13
20
|
|
|
14
21
|
export const postSynthChecks: PostSynthCheck[] = [
|
|
15
22
|
cedc010,
|
|
@@ -22,4 +29,11 @@ export const postSynthChecks: PostSynthCheck[] = [
|
|
|
22
29
|
ceds010,
|
|
23
30
|
ceds011,
|
|
24
31
|
ceds012,
|
|
32
|
+
dwdc010,
|
|
33
|
+
dwdc011,
|
|
34
|
+
dwdc012,
|
|
35
|
+
dwdc013,
|
|
36
|
+
dwde010,
|
|
37
|
+
dwde011,
|
|
38
|
+
dwds010,
|
|
25
39
|
];
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { describe, test, expect } from "vitest";
|
|
13
13
|
import { createPostSynthContext, makePostSynthCtxFromFiles } from "@intentius/chant-test-utils";
|
|
14
14
|
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
15
|
-
import { CEDAR_JSON_FILENAME } from "../../serializer";
|
|
15
|
+
import { CEDAR_JSON_FILENAME, cedarSerializer } from "../../serializer";
|
|
16
16
|
import { postSynthChecks } from ".";
|
|
17
17
|
import { cedc010 } from "./cedc010";
|
|
18
18
|
import { cedc011 } from "./cedc011";
|
|
@@ -103,10 +103,14 @@ function ctxWithSchema(
|
|
|
103
103
|
// ── Barrel ─────────────────────────────────────────────────────────
|
|
104
104
|
|
|
105
105
|
describe("the cedar post-synth barrel", () => {
|
|
106
|
-
test("ships every check exactly once,
|
|
106
|
+
test("ships every check exactly once, under a declared prefix", () => {
|
|
107
|
+
// Two id families, both declared on the serializer: CED for Cedar itself,
|
|
108
|
+
// DWD for the dogwood temporal dialect that ships inside this lexicon
|
|
109
|
+
// (#1658). An id outside both is what `chant dev check-lexicon` fails on.
|
|
110
|
+
const declared = [cedarSerializer.rulePrefix, ...(cedarSerializer.extraRulePrefixes ?? [])];
|
|
107
111
|
const ids = postSynthChecks.map((c) => c.id);
|
|
108
112
|
expect(new Set(ids).size).toBe(ids.length);
|
|
109
|
-
expect(ids.
|
|
113
|
+
expect(ids.filter((id) => !declared.some((p) => id.startsWith(p)))).toEqual([]);
|
|
110
114
|
expect(ids.length).toBeGreaterThanOrEqual(10);
|
|
111
115
|
});
|
|
112
116
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cedar Op activities — resolved by the core activity registry when a
|
|
3
|
+
* project's `chant.config.ts` lists the `cedar` lexicon.
|
|
4
|
+
*
|
|
5
|
+
* The registry keys every exported *function* in this module by its name
|
|
6
|
+
* (`loadActivities` → `collectActivities`), which is why only the activities
|
|
7
|
+
* themselves are exported here. `dogwoodReplay`'s helpers — the input
|
|
8
|
+
* resolver, the verdict comparison, the summary renderer — stay importable
|
|
9
|
+
* from `@intentius/chant-lexicon-cedar/dogwood/replay-activity` rather than
|
|
10
|
+
* being registered as activities nobody would ever name in a step.
|
|
11
|
+
*
|
|
12
|
+
* Contributed the `flyApply` way: a plain async function taking one args
|
|
13
|
+
* object, with no Temporal import anywhere beneath it, so the local executor
|
|
14
|
+
* runs it unchanged and a Temporal worker registers the same function.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export { dogwoodReplay, dogwoodReplayReport } from "../../dogwood/replay-activity";
|
|
18
|
+
export type {
|
|
19
|
+
DogwoodReplayArgs,
|
|
20
|
+
DogwoodReplayReportArgs,
|
|
21
|
+
ExpectedVerdict,
|
|
22
|
+
PolicyReplayDispatch,
|
|
23
|
+
PolicyReplayMode,
|
|
24
|
+
PolicyReplayReport,
|
|
25
|
+
ReplayDivergence,
|
|
26
|
+
ReplayExpectation,
|
|
27
|
+
} from "../../dogwood/replay-activity";
|
package/src/plugin.test.ts
CHANGED
|
@@ -43,9 +43,9 @@ describe("cedar plugin", () => {
|
|
|
43
43
|
}
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
it("ships
|
|
46
|
+
it("ships four skills whose frontmatter matches their registered name", () => {
|
|
47
47
|
const skills = cedarPlugin.skills?.() ?? [];
|
|
48
|
-
expect(skills).toHaveLength(
|
|
48
|
+
expect(skills).toHaveLength(4);
|
|
49
49
|
|
|
50
50
|
for (const skill of skills) {
|
|
51
51
|
// An empty body means the loader could not read the file — the skill
|
|
@@ -59,6 +59,7 @@ describe("cedar plugin", () => {
|
|
|
59
59
|
expect(skills.map((s) => s.name).sort()).toEqual([
|
|
60
60
|
"chant-cedar-authoring",
|
|
61
61
|
"chant-cedar-avp-embedding",
|
|
62
|
+
"chant-cedar-dogwood",
|
|
62
63
|
"chant-cedar-meta-policy",
|
|
63
64
|
]);
|
|
64
65
|
});
|
package/src/plugin.ts
CHANGED
|
@@ -153,6 +153,36 @@ export const cedarPlugin: LexiconPlugin = {
|
|
|
153
153
|
},
|
|
154
154
|
],
|
|
155
155
|
},
|
|
156
|
+
{
|
|
157
|
+
file: "chant-cedar-dogwood.md",
|
|
158
|
+
name: "chant-cedar-dogwood",
|
|
159
|
+
description:
|
|
160
|
+
"Author dogwood temporal policies with the typed builders — parser primitives versus default-library macros, the callerPrincipal pin, the CLI-gated validation split, AgentCore embedding and trace replay",
|
|
161
|
+
triggers: [
|
|
162
|
+
{ type: "file-pattern", value: "**/*.dw" },
|
|
163
|
+
{ type: "file-pattern", value: "**/*.dwschema" },
|
|
164
|
+
{ type: "context", value: "dogwood" },
|
|
165
|
+
{ type: "context", value: "temporal policy" },
|
|
166
|
+
{ type: "context", value: "agentcore policy" },
|
|
167
|
+
{ type: "context", value: "rate limit policy" },
|
|
168
|
+
{ type: "context", value: "policy replay" },
|
|
169
|
+
],
|
|
170
|
+
parameters: [],
|
|
171
|
+
examples: [
|
|
172
|
+
{
|
|
173
|
+
title: "Approval before action",
|
|
174
|
+
description: "Allow a read only if a login happened inside the window",
|
|
175
|
+
input: "Only let them read if they logged in within the last hour",
|
|
176
|
+
output: `whenTemporal: [dogwood.formerly("1h", dogwood.predicate('Drupe::Action::"Login"', "response"))]`,
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
title: "The macro caveat",
|
|
180
|
+
input: "Use count_within for a rate limit",
|
|
181
|
+
output:
|
|
182
|
+
"count_within is a default-library macro a --macros caller replaces — emit the call and ship the definition, or build it from `count for … where`",
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
},
|
|
156
186
|
]),
|
|
157
187
|
|
|
158
188
|
initTemplates(template?: string): InitTemplateSet {
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cedar policy-text rendering, shared by the `.cedar` serializer, the AVP
|
|
3
|
+
* statement renderer, and the dogwood dialect's `.dw` leg.
|
|
4
|
+
*
|
|
5
|
+
* Upstream's dogwood policy grammar is Cedar's shape — `annotation* effect
|
|
6
|
+
* "(" scope ")" cond* ";"` — and only the `cond` rule differs, gaining
|
|
7
|
+
* `guardrails { … }` and `temporal { … }` forms. So everything above the first
|
|
8
|
+
* clause is common, and it lives here rather than being copied into
|
|
9
|
+
* `./dogwood/serialize.ts`, where a change to how a scope constraint is
|
|
10
|
+
* written would have had to be made twice.
|
|
11
|
+
*
|
|
12
|
+
* `resolvePolicyId` is here for the same reason it was extracted from
|
|
13
|
+
* `serialize()` by #1652: it is the only rule linking a chant entity to a
|
|
14
|
+
* policy id, the AVP observation matches against it, and the `.dw` leg has to
|
|
15
|
+
* derive ids the same way the `.cedar` leg does. One copy, three callers.
|
|
16
|
+
*
|
|
17
|
+
* This module is a leaf on purpose: `./serializer.ts` and `./dogwood/*` both
|
|
18
|
+
* import it, and neither imports the other's rendering.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
22
|
+
import { isResourceDeclarable } from "@intentius/chant/declarable";
|
|
23
|
+
|
|
24
|
+
/** A plain object — not null, not an array. */
|
|
25
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
26
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The `props` of a resource-kind Declarable, or `{}` for anything else. */
|
|
30
|
+
export function getProps(entity: Declarable): Record<string, unknown> {
|
|
31
|
+
if (isResourceDeclarable(entity) && isRecord(entity.props)) {
|
|
32
|
+
return entity.props;
|
|
33
|
+
}
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Escape a string for a double-quoted Cedar literal. */
|
|
38
|
+
export function escapeCedarString(value: string): string {
|
|
39
|
+
return value
|
|
40
|
+
.replace(/\\/g, "\\\\")
|
|
41
|
+
.replace(/"/g, '\\"')
|
|
42
|
+
.replace(/\n/g, "\\n")
|
|
43
|
+
.replace(/\r/g, "\\r")
|
|
44
|
+
.replace(/\t/g, "\\t");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Derive a policy id from a logical name: `allowAdminRead` → `allow-admin-read`.
|
|
49
|
+
* Cedar ids are free-form strings; kebab-case keeps them readable in the
|
|
50
|
+
* `@id` annotation and stable across a rename-free refactor.
|
|
51
|
+
*/
|
|
52
|
+
export function policyIdFromLogicalName(name: string): string {
|
|
53
|
+
return name
|
|
54
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
55
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
|
|
56
|
+
.replace(/[\s_]+/g, "-")
|
|
57
|
+
.toLowerCase();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The Cedar id for a policy: an explicit `annotations.id` when the author gave
|
|
62
|
+
* one, else derived from the logical name.
|
|
63
|
+
*
|
|
64
|
+
* This is the only rule that links a chant entity to a policy in a live AVP
|
|
65
|
+
* store (#1652) — the observation resolves the same id from the same props and
|
|
66
|
+
* matches it against the `@id` annotation the statement carries — and it is
|
|
67
|
+
* also what gives a `.dw` policy the id its `.cedar` sibling would have had.
|
|
68
|
+
* Two copies of this rule would be a mapping that drifts silently.
|
|
69
|
+
*/
|
|
70
|
+
export function resolvePolicyId(logicalName: string, props: Record<string, unknown>): string {
|
|
71
|
+
const explicit = isRecord(props.annotations) ? props.annotations.id : undefined;
|
|
72
|
+
return typeof explicit === "string" && explicit.length > 0
|
|
73
|
+
? explicit
|
|
74
|
+
: policyIdFromLogicalName(logicalName);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function refText(value: unknown): string {
|
|
78
|
+
return String(value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** `principal`, `principal == User::"alice"`, `action in [ … ]`, `resource is Photo`. */
|
|
82
|
+
export function renderScope(variable: string, scope: unknown): string {
|
|
83
|
+
if (!isRecord(scope)) return variable;
|
|
84
|
+
|
|
85
|
+
const parts = [variable];
|
|
86
|
+
if (typeof scope.is === "string") parts.push(`is ${scope.is}`);
|
|
87
|
+
if (scope.eq !== undefined) parts.push(`== ${refText(scope.eq)}`);
|
|
88
|
+
if (scope.in !== undefined) {
|
|
89
|
+
parts.push(
|
|
90
|
+
Array.isArray(scope.in)
|
|
91
|
+
? `in [${scope.in.map(refText).join(", ")}]`
|
|
92
|
+
: `in ${refText(scope.in)}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return parts.join(" ");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A `when`/`unless` prop as a list of clause bodies — one string, or several. */
|
|
99
|
+
export function conditionStrings(value: unknown): string[] {
|
|
100
|
+
if (value === undefined || value === null) return [];
|
|
101
|
+
if (Array.isArray(value)) return value.map(refText);
|
|
102
|
+
return [refText(value)];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Everything above the first clause: the annotations, the effect, and the
|
|
107
|
+
* three scope positions.
|
|
108
|
+
*/
|
|
109
|
+
export function renderPolicyHead(id: string, props: Record<string, unknown>): string[] {
|
|
110
|
+
const lines: string[] = [];
|
|
111
|
+
|
|
112
|
+
// @id first, then the author's own annotations in declaration order.
|
|
113
|
+
const annotations = isRecord(props.annotations) ? props.annotations : {};
|
|
114
|
+
lines.push(`@id("${escapeCedarString(id)}")`);
|
|
115
|
+
for (const [key, value] of Object.entries(annotations)) {
|
|
116
|
+
if (key === "id" || value === undefined || value === null) continue;
|
|
117
|
+
lines.push(`@${key}("${escapeCedarString(refText(value))}")`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const effect = props.effect === "forbid" ? "forbid" : "permit";
|
|
121
|
+
lines.push(`${effect} (`);
|
|
122
|
+
lines.push(` ${renderScope("principal", props.principal)},`);
|
|
123
|
+
lines.push(` ${renderScope("action", props.action)},`);
|
|
124
|
+
lines.push(` ${renderScope("resource", props.resource)}`);
|
|
125
|
+
lines.push(")");
|
|
126
|
+
|
|
127
|
+
return lines;
|
|
128
|
+
}
|