@intentius/chant-lexicon-cedar 0.44.8
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 +190 -0
- package/dist/avp/ambient.d.ts +54 -0
- package/dist/avp/ambient.d.ts.map +1 -0
- package/dist/avp/client.d.ts +127 -0
- package/dist/avp/client.d.ts.map +1 -0
- package/dist/avp/describe-resources.d.ts +42 -0
- package/dist/avp/describe-resources.d.ts.map +1 -0
- package/dist/avp/embed.d.ts +120 -0
- package/dist/avp/embed.d.ts.map +1 -0
- package/dist/avp/live-export.d.ts +94 -0
- package/dist/avp/live-export.d.ts.map +1 -0
- package/dist/avp/ownership.d.ts +97 -0
- package/dist/avp/ownership.d.ts.map +1 -0
- package/dist/avp/statement.d.ts +22 -0
- package/dist/avp/statement.d.ts.map +1 -0
- package/dist/avp/store.d.ts +82 -0
- package/dist/avp/store.d.ts.map +1 -0
- package/dist/avp/testdata/mock-transport.d.ts +55 -0
- package/dist/avp/testdata/mock-transport.d.ts.map +1 -0
- package/dist/codegen/docs-cli.d.ts +3 -0
- package/dist/codegen/docs-cli.d.ts.map +1 -0
- package/dist/codegen/docs.d.ts +26 -0
- package/dist/codegen/docs.d.ts.map +1 -0
- package/dist/codegen/emit.d.ts +85 -0
- package/dist/codegen/emit.d.ts.map +1 -0
- package/dist/codegen/generate-cli.d.ts +3 -0
- package/dist/codegen/generate-cli.d.ts.map +1 -0
- package/dist/codegen/generate.d.ts +37 -0
- package/dist/codegen/generate.d.ts.map +1 -0
- package/dist/codegen/naming.d.ts +48 -0
- package/dist/codegen/naming.d.ts.map +1 -0
- package/dist/codegen/package.d.ts +10 -0
- package/dist/codegen/package.d.ts.map +1 -0
- package/dist/composites/deny-by-default-set.d.ts +58 -0
- package/dist/composites/deny-by-default-set.d.ts.map +1 -0
- package/dist/composites/index.d.ts +12 -0
- package/dist/composites/index.d.ts.map +1 -0
- package/dist/composites/owner-can-manage.d.ts +48 -0
- package/dist/composites/owner-can-manage.d.ts.map +1 -0
- package/dist/config.d.ts +102 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/coverage.d.ts +59 -0
- package/dist/coverage.d.ts.map +1 -0
- package/dist/detect.d.ts +18 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/generated/index.d.ts +251 -0
- package/dist/generated/index.d.ts.map +1 -0
- package/dist/generated/runtime.d.ts +2 -0
- package/dist/generated/runtime.d.ts.map +1 -0
- package/dist/import/adapter.d.ts +21 -0
- package/dist/import/adapter.d.ts.map +1 -0
- package/dist/import/clause-text.d.ts +38 -0
- package/dist/import/clause-text.d.ts.map +1 -0
- package/dist/import/generator.d.ts +53 -0
- package/dist/import/generator.d.ts.map +1 -0
- package/dist/import/parser.d.ts +57 -0
- package/dist/import/parser.d.ts.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/init-templates.d.ts +32 -0
- package/dist/init-templates.d.ts.map +1 -0
- package/dist/integrity.json +22 -0
- package/dist/lint/audit-catalog.d.ts +29 -0
- package/dist/lint/audit-catalog.d.ts.map +1 -0
- package/dist/lint/post-synth/cedar-helpers.d.ts +97 -0
- package/dist/lint/post-synth/cedar-helpers.d.ts.map +1 -0
- package/dist/lint/post-synth/cedc010.d.ts +17 -0
- package/dist/lint/post-synth/cedc010.d.ts.map +1 -0
- package/dist/lint/post-synth/cedc011.d.ts +17 -0
- package/dist/lint/post-synth/cedc011.d.ts.map +1 -0
- package/dist/lint/post-synth/cedc012.d.ts +21 -0
- package/dist/lint/post-synth/cedc012.d.ts.map +1 -0
- package/dist/lint/post-synth/cedc013.d.ts +20 -0
- package/dist/lint/post-synth/cedc013.d.ts.map +1 -0
- package/dist/lint/post-synth/cedc014.d.ts +19 -0
- package/dist/lint/post-synth/cedc014.d.ts.map +1 -0
- package/dist/lint/post-synth/cede010.d.ts +34 -0
- package/dist/lint/post-synth/cede010.d.ts.map +1 -0
- package/dist/lint/post-synth/cede011.d.ts +22 -0
- package/dist/lint/post-synth/cede011.d.ts.map +1 -0
- package/dist/lint/post-synth/ceds010.d.ts +24 -0
- package/dist/lint/post-synth/ceds010.d.ts.map +1 -0
- package/dist/lint/post-synth/ceds011.d.ts +19 -0
- package/dist/lint/post-synth/ceds011.d.ts.map +1 -0
- package/dist/lint/post-synth/ceds012.d.ts +17 -0
- package/dist/lint/post-synth/ceds012.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts +3 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -0
- package/dist/lint/post-synth/wasm-helpers.d.ts +108 -0
- package/dist/lint/post-synth/wasm-helpers.d.ts.map +1 -0
- package/dist/lint/rules/index.d.ts +10 -0
- package/dist/lint/rules/index.d.ts.map +1 -0
- package/dist/lint/rules/policy-shape.d.ts +31 -0
- package/dist/lint/rules/policy-shape.d.ts.map +1 -0
- package/dist/lsp/completions.d.ts +24 -0
- package/dist/lsp/completions.d.ts.map +1 -0
- package/dist/lsp/hover.d.ts +11 -0
- package/dist/lsp/hover.d.ts.map +1 -0
- package/dist/lsp/registry.d.ts +45 -0
- package/dist/lsp/registry.d.ts.map +1 -0
- package/dist/manifest.json +8 -0
- package/dist/mcp/index.d.ts +34 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/policy-coverage.d.ts +81 -0
- package/dist/mcp/policy-coverage.d.ts.map +1 -0
- package/dist/meta.json +699 -0
- package/dist/okf/index.md +37 -0
- package/dist/okf/rules/CEDC001.md +11 -0
- package/dist/okf/rules/CEDC010.md +11 -0
- package/dist/okf/rules/CEDC011.md +15 -0
- package/dist/okf/rules/CEDC012.md +11 -0
- package/dist/okf/rules/CEDC013.md +15 -0
- package/dist/okf/rules/CEDC014.md +15 -0
- package/dist/okf/rules/CEDE010.md +15 -0
- package/dist/okf/rules/CEDE011.md +15 -0
- package/dist/okf/rules/CEDS010.md +15 -0
- package/dist/okf/rules/CEDS011.md +11 -0
- package/dist/okf/rules/CEDS012.md +15 -0
- package/dist/okf/types/AdminAction.md +14 -0
- package/dist/okf/types/Application.md +14 -0
- package/dist/okf/types/ApproveAction.md +13 -0
- package/dist/okf/types/CommentAction.md +14 -0
- package/dist/okf/types/CreateAction.md +14 -0
- package/dist/okf/types/DeleteAction.md +14 -0
- package/dist/okf/types/Document.md +18 -0
- package/dist/okf/types/Folder.md +15 -0
- package/dist/okf/types/Group.md +14 -0
- package/dist/okf/types/ListAction.md +14 -0
- package/dist/okf/types/Policy.md +29 -0
- package/dist/okf/types/ReadAction.md +14 -0
- package/dist/okf/types/ServiceAccount.md +15 -0
- package/dist/okf/types/ShareAction.md +14 -0
- package/dist/okf/types/Team.md +14 -0
- package/dist/okf/types/User.md +18 -0
- package/dist/okf/types/WriteAction.md +14 -0
- package/dist/package-cli.d.ts +3 -0
- package/dist/package-cli.d.ts.map +1 -0
- package/dist/plugin.d.ts +8 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/rules/cedar-helpers.ts +209 -0
- package/dist/rules/cedc010.ts +80 -0
- package/dist/rules/cedc011.ts +47 -0
- package/dist/rules/cedc012.ts +64 -0
- package/dist/rules/cedc013.ts +68 -0
- package/dist/rules/cedc014.ts +73 -0
- package/dist/rules/cede010.ts +89 -0
- package/dist/rules/cede011.ts +58 -0
- package/dist/rules/ceds010.ts +57 -0
- package/dist/rules/ceds011.ts +42 -0
- package/dist/rules/ceds012.ts +50 -0
- package/dist/rules/policy-shape.ts +148 -0
- package/dist/rules/wasm-helpers.ts +315 -0
- package/dist/serializer.d.ts +138 -0
- package/dist/serializer.d.ts.map +1 -0
- package/dist/spec/fetch.d.ts +71 -0
- package/dist/spec/fetch.d.ts.map +1 -0
- package/dist/spec/parse.d.ts +113 -0
- package/dist/spec/parse.d.ts.map +1 -0
- package/dist/spec/pin.d.ts +116 -0
- package/dist/spec/pin.d.ts.map +1 -0
- package/dist/spec/pinned-names.json +18 -0
- package/dist/spec/wasm.d.ts +143 -0
- package/dist/spec/wasm.d.ts.map +1 -0
- package/dist/types/index.d.ts +219 -0
- package/dist/validate-cli.d.ts +3 -0
- package/dist/validate-cli.d.ts.map +1 -0
- package/dist/validate.d.ts +25 -0
- package/dist/validate.d.ts.map +1 -0
- package/package.json +75 -0
- package/src/avp/OWNERSHIP.md +125 -0
- package/src/avp/ambient.test.ts +113 -0
- package/src/avp/ambient.ts +124 -0
- package/src/avp/client.ts +310 -0
- package/src/avp/describe-resources.test.ts +316 -0
- package/src/avp/describe-resources.ts +215 -0
- package/src/avp/embed.test.ts +114 -0
- package/src/avp/embed.ts +190 -0
- package/src/avp/live-export.test.ts +232 -0
- package/src/avp/live-export.ts +185 -0
- package/src/avp/ownership.test.ts +101 -0
- package/src/avp/ownership.ts +170 -0
- package/src/avp/statement.ts +50 -0
- package/src/avp/store.ts +152 -0
- package/src/avp/testdata/mock-transport.ts +147 -0
- package/src/codegen/docs-cli.ts +7 -0
- package/src/codegen/docs.ts +873 -0
- package/src/codegen/emit.ts +496 -0
- package/src/codegen/generate-cli.ts +18 -0
- package/src/codegen/generate.test.ts +128 -0
- package/src/codegen/generate.ts +123 -0
- package/src/codegen/naming.ts +101 -0
- package/src/codegen/package.ts +52 -0
- package/src/composites/composites.test.ts +206 -0
- package/src/composites/deny-by-default-set.ts +98 -0
- package/src/composites/index.ts +13 -0
- package/src/composites/owner-can-manage.ts +80 -0
- package/src/config-namespace.test.ts +44 -0
- package/src/config.test.ts +82 -0
- package/src/config.ts +119 -0
- package/src/coverage.test.ts +61 -0
- package/src/coverage.ts +166 -0
- package/src/detect.test.ts +64 -0
- package/src/detect.ts +59 -0
- package/src/generated/index.d.ts +219 -0
- package/src/generated/index.ts +279 -0
- package/src/generated/lexicon-cedar.json +699 -0
- package/src/generated/runtime.ts +2 -0
- package/src/import/adapter.ts +63 -0
- package/src/import/clause-text.ts +178 -0
- package/src/import/generator.test.ts +133 -0
- package/src/import/generator.ts +219 -0
- package/src/import/parser.test.ts +265 -0
- package/src/import/parser.ts +352 -0
- package/src/import/roundtrip.test.ts +127 -0
- package/src/import/testdata/full.cedar +36 -0
- package/src/import/testdata/full.cedar.json +238 -0
- package/src/import/testdata/realistic.cedar +35 -0
- package/src/import/testdata/simple.cedar +6 -0
- package/src/index.ts +94 -0
- package/src/init-templates.test.ts +158 -0
- package/src/init-templates.ts +358 -0
- package/src/lint/audit-catalog.ts +130 -0
- package/src/lint/post-synth/cedar-helpers.ts +209 -0
- package/src/lint/post-synth/cedc010.ts +80 -0
- package/src/lint/post-synth/cedc011.ts +47 -0
- package/src/lint/post-synth/cedc012.ts +64 -0
- package/src/lint/post-synth/cedc013.ts +68 -0
- package/src/lint/post-synth/cedc014.ts +73 -0
- package/src/lint/post-synth/cede010.ts +89 -0
- package/src/lint/post-synth/cede011.ts +58 -0
- package/src/lint/post-synth/ceds010.ts +57 -0
- package/src/lint/post-synth/ceds011.ts +42 -0
- package/src/lint/post-synth/ceds012.ts +50 -0
- package/src/lint/post-synth/index.ts +25 -0
- package/src/lint/post-synth/post-synth.test.ts +474 -0
- package/src/lint/post-synth/wasm-helpers.ts +315 -0
- package/src/lint/rules/index.ts +12 -0
- package/src/lint/rules/policy-shape.test.ts +90 -0
- package/src/lint/rules/policy-shape.ts +148 -0
- package/src/lsp/completions.test.ts +111 -0
- package/src/lsp/completions.ts +55 -0
- package/src/lsp/hover.test.ts +82 -0
- package/src/lsp/hover.ts +75 -0
- package/src/lsp/registry.ts +74 -0
- package/src/mcp/index.ts +103 -0
- package/src/mcp/policy-coverage.test.ts +176 -0
- package/src/mcp/policy-coverage.ts +205 -0
- package/src/package-cli.ts +22 -0
- package/src/plugin.test.ts +115 -0
- package/src/plugin.ts +312 -0
- package/src/serializer.test.ts +502 -0
- package/src/serializer.ts +335 -0
- package/src/skills/chant-cedar-authoring.md +180 -0
- package/src/skills/chant-cedar-avp-embedding.md +125 -0
- package/src/skills/chant-cedar-meta-policy.md +119 -0
- package/src/spec/default-schema.cedarschema +108 -0
- package/src/spec/fetch.ts +107 -0
- package/src/spec/parse.test.ts +123 -0
- package/src/spec/parse.ts +253 -0
- package/src/spec/pin.test.ts +88 -0
- package/src/spec/pin.ts +243 -0
- package/src/spec/pinned-names.json +18 -0
- package/src/spec/wasm.ts +283 -0
- package/src/validate-cli.ts +8 -0
- package/src/validate.ts +85 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { CompletionContext, CompletionItem } from "@intentius/chant/lsp/types";
|
|
2
|
+
import { lexiconCompletions } from "@intentius/chant/lsp/lexicon-providers";
|
|
3
|
+
import { cedarIndex, cedarRegistry } from "./registry";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Completions over the schema-derived registry.
|
|
7
|
+
*
|
|
8
|
+
* Two shapes come out of core's generic provider: class names after `new `,
|
|
9
|
+
* and property names inside a constructor's object literal. Both are useful
|
|
10
|
+
* here — `new Policy({` completes `effect`/`principal`/`action`/…, and
|
|
11
|
+
* `new Doc` completes `Document`.
|
|
12
|
+
*
|
|
13
|
+
* The third shape is Cedar's own: a scope names an *action constant*, not a
|
|
14
|
+
* class, and those are plain `const` exports rather than constructors, so no
|
|
15
|
+
* `new ` ever precedes them. {@link actionCompletions} covers that position.
|
|
16
|
+
*/
|
|
17
|
+
export function completions(ctx: CompletionContext): CompletionItem[] {
|
|
18
|
+
const generic = lexiconCompletions(ctx, cedarIndex(), "Cedar declaration");
|
|
19
|
+
if (generic.length > 0) return generic;
|
|
20
|
+
return actionCompletions(ctx);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Action constants in a scope position.
|
|
25
|
+
*
|
|
26
|
+
* `action: { eq: <cursor> }` and `action: { in: [<cursor>` are where an action
|
|
27
|
+
* is named, and the generated constants (`ReadAction`, `WriteAction`, …) are
|
|
28
|
+
* the only correct values. Matching on the surrounding text rather than on a
|
|
29
|
+
* parsed AST keeps this cheap and is what the other lexicons' providers do.
|
|
30
|
+
*/
|
|
31
|
+
export function actionCompletions(ctx: CompletionContext): CompletionItem[] {
|
|
32
|
+
const linePrefix = ctx.linePrefix ?? "";
|
|
33
|
+
if (!/\baction\s*:\s*\{\s*(eq|in)\s*:\s*\[?\s*[A-Za-z]*$/.test(linePrefix)) return [];
|
|
34
|
+
|
|
35
|
+
const prefix = (ctx.wordAtCursor ?? "").toLowerCase();
|
|
36
|
+
const registry = cedarRegistry();
|
|
37
|
+
|
|
38
|
+
return Object.entries(registry)
|
|
39
|
+
.filter(([, entry]) => entry.kind === "resource" && entry.resourceType.includes("::Action::"))
|
|
40
|
+
.filter(([className]) => !prefix || className.toLowerCase().startsWith(prefix))
|
|
41
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
42
|
+
.map(([className, entry]) => ({
|
|
43
|
+
label: className,
|
|
44
|
+
insertText: className,
|
|
45
|
+
kind: "resource" as const,
|
|
46
|
+
detail: entry.resourceType,
|
|
47
|
+
documentation: [
|
|
48
|
+
`Cedar action: \`${entry.resourceType}\``,
|
|
49
|
+
entry.principalTypes?.length ? `Principals: ${entry.principalTypes.join(", ")}` : "",
|
|
50
|
+
entry.resourceTypes?.length ? `Resources: ${entry.resourceTypes.join(", ")}` : "",
|
|
51
|
+
]
|
|
52
|
+
.filter(Boolean)
|
|
53
|
+
.join("\n"),
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import type { HoverContext } from "@intentius/chant/lsp/types";
|
|
6
|
+
import { hover } from "./hover";
|
|
7
|
+
|
|
8
|
+
const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
9
|
+
const lexiconPath = join(pkgDir, "src", "generated", "lexicon-cedar.json");
|
|
10
|
+
const hasGenerated =
|
|
11
|
+
existsSync(lexiconPath) &&
|
|
12
|
+
(() => {
|
|
13
|
+
try {
|
|
14
|
+
return Object.keys(JSON.parse(readFileSync(lexiconPath, "utf-8"))).length > 0;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
})();
|
|
19
|
+
|
|
20
|
+
function ctx(word: string): HoverContext {
|
|
21
|
+
return {
|
|
22
|
+
uri: "file:///policies.ts",
|
|
23
|
+
content: "",
|
|
24
|
+
position: { line: 0, character: 0 },
|
|
25
|
+
word,
|
|
26
|
+
lineText: "",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("cedar hover", () => {
|
|
31
|
+
test("returns undefined for a word the registry does not know", () => {
|
|
32
|
+
expect(hover(ctx("NotACedarThing"))).toBeUndefined();
|
|
33
|
+
expect(hover(ctx(""))).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test.skipIf(!hasGenerated)("describes Policy with its prop table", () => {
|
|
37
|
+
const info = hover(ctx("Policy"));
|
|
38
|
+
|
|
39
|
+
expect(info?.contents).toContain("**Policy**");
|
|
40
|
+
expect(info?.contents).toContain("Cedar::Policy");
|
|
41
|
+
expect(info?.contents).toContain("`effect`");
|
|
42
|
+
expect(info?.contents).toContain("permit");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test.skipIf(!hasGenerated)("describes an entity type with its attributes", () => {
|
|
46
|
+
const info = hover(ctx("Document"));
|
|
47
|
+
|
|
48
|
+
expect(info?.contents).toContain("App::Document");
|
|
49
|
+
expect(info?.contents).toContain("**Attributes:**");
|
|
50
|
+
expect(info?.contents).toContain("classification");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test.skipIf(!hasGenerated)("marks an optional attribute", () => {
|
|
54
|
+
const info = hover(ctx("User"));
|
|
55
|
+
|
|
56
|
+
// `manager` is declared `"manager"?: User` in the default schema.
|
|
57
|
+
expect(info?.contents).toContain("`manager`? —");
|
|
58
|
+
expect(info?.contents).toContain("`email` —");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test.skipIf(!hasGenerated)("reports what an entity type is a member of", () => {
|
|
62
|
+
expect(hover(ctx("Team"))?.contents).toContain("**Member of:** `App::Group`");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test.skipIf(!hasGenerated)("describes an action's principals, resources and context", () => {
|
|
66
|
+
const info = hover(ctx("ReadAction"));
|
|
67
|
+
|
|
68
|
+
expect(info?.contents).toContain('App::Action::"read"');
|
|
69
|
+
expect(info?.contents).toContain("**Principals:**");
|
|
70
|
+
expect(info?.contents).toContain("App::ServiceAccount");
|
|
71
|
+
expect(info?.contents).toContain("**Resources:**");
|
|
72
|
+
expect(info?.contents).toContain("**Context:**");
|
|
73
|
+
expect(info?.contents).toContain("mfa");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test.skipIf(!hasGenerated)("describes a record property type as fields", () => {
|
|
77
|
+
const info = hover(ctx("DocumentAttributes"));
|
|
78
|
+
|
|
79
|
+
expect(info?.contents).toContain("App::Document.Attributes");
|
|
80
|
+
expect(info?.contents).toContain("**Fields:**");
|
|
81
|
+
});
|
|
82
|
+
});
|
package/src/lsp/hover.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { HoverContext, HoverInfo } from "@intentius/chant/lsp/types";
|
|
2
|
+
import { lexiconHover, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
|
|
3
|
+
import { cedarIndex, cedarRegistry } from "./registry";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hover over a generated declaration.
|
|
7
|
+
*
|
|
8
|
+
* Core's default formatter reports CloudFormation-shaped facts (primary
|
|
9
|
+
* identifier, create-only, write-only) a Cedar declaration does not have. The
|
|
10
|
+
* facts that matter here are the Cedar ones: what a `Policy` prop means, which
|
|
11
|
+
* groups an entity type is `in`, and what an action applies to.
|
|
12
|
+
*/
|
|
13
|
+
export function hover(ctx: HoverContext): HoverInfo | undefined {
|
|
14
|
+
return lexiconHover(ctx, cedarIndex(), formatCedarHover);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatCedarHover(className: string, indexed: LexiconEntry): HoverInfo | undefined {
|
|
18
|
+
const entry = cedarRegistry()[className];
|
|
19
|
+
if (!entry) return undefined;
|
|
20
|
+
|
|
21
|
+
const lines = [`**${className}**`, "", `Cedar type: \`${entry.resourceType}\``];
|
|
22
|
+
|
|
23
|
+
if (entry.description) {
|
|
24
|
+
lines.push("", entry.description);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (entry.resourceType === "Cedar::Policy") {
|
|
28
|
+
lines.push(
|
|
29
|
+
"",
|
|
30
|
+
"The unit this lexicon serializes — one `.cedar` policy plus its entry in the JSON policy set.",
|
|
31
|
+
"",
|
|
32
|
+
"| Prop | Meaning |",
|
|
33
|
+
"|------|---------|",
|
|
34
|
+
"| `effect` | `permit` or `forbid`; defaults to `permit` |",
|
|
35
|
+
"| `principal`, `action`, `resource` | scope constraints — `{}`, `{ eq }`, `{ in }`, `{ is }` |",
|
|
36
|
+
"| `when`, `unless` | Cedar expression strings, one clause each |",
|
|
37
|
+
"| `annotations` | emitted as `@key(\"value\")` above the policy |",
|
|
38
|
+
);
|
|
39
|
+
return { contents: lines.join("\n") };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (entry.resourceType.includes("::Action::")) {
|
|
43
|
+
if (entry.principalTypes?.length) {
|
|
44
|
+
lines.push("", `**Principals:** ${entry.principalTypes.map((t) => `\`${t}\``).join(", ")}`);
|
|
45
|
+
}
|
|
46
|
+
if (entry.resourceTypes?.length) {
|
|
47
|
+
lines.push("", `**Resources:** ${entry.resourceTypes.map((t) => `\`${t}\``).join(", ")}`);
|
|
48
|
+
}
|
|
49
|
+
const context = Object.entries(entry.properties ?? {});
|
|
50
|
+
if (context.length > 0) {
|
|
51
|
+
lines.push("", "**Context:**");
|
|
52
|
+
for (const [name, spec] of context) {
|
|
53
|
+
lines.push(`- \`${name}\`${spec.required ? "" : "?"} — \`${spec.type}\``);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { contents: lines.join("\n") };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (entry.memberOfTypes?.length) {
|
|
60
|
+
lines.push("", `**Member of:** ${entry.memberOfTypes.map((t) => `\`${t}\``).join(", ")}`);
|
|
61
|
+
}
|
|
62
|
+
if (entry.enumValues?.length) {
|
|
63
|
+
lines.push("", `**Enum values:** ${entry.enumValues.map((v) => `\`${v}\``).join(", ")}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const attributes = Object.entries(entry.properties ?? {});
|
|
67
|
+
if (attributes.length > 0) {
|
|
68
|
+
lines.push("", indexed.kind === "property" ? "**Fields:**" : "**Attributes:**");
|
|
69
|
+
for (const [name, spec] of attributes) {
|
|
70
|
+
lines.push(`- \`${name}\`${spec.required ? "" : "?"} — \`${spec.type}\``);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { contents: lines.join("\n") };
|
|
75
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The generated registry, adapted to what core's `LexiconIndex` reads.
|
|
3
|
+
*
|
|
4
|
+
* `lexicon-cedar.json` describes every declaration the project's schema
|
|
5
|
+
* produced (#1650): the `Policy` authoring class, one entry per entity type,
|
|
6
|
+
* one per action, and the attribute/context record types beside them. Its
|
|
7
|
+
* property table is keyed `properties`, while `LexiconIndex.getPropertyNames`
|
|
8
|
+
* looks at `propertyConstraints` — the CloudFormation-shaped name the class was
|
|
9
|
+
* written against. Renaming the generated key to match would put a
|
|
10
|
+
* CloudFormation word in a Cedar artifact; translating here does not, and it is
|
|
11
|
+
* the only reason this module exists.
|
|
12
|
+
*
|
|
13
|
+
* The require is lazy and cached. `generate()` writes the registry, so a
|
|
14
|
+
* checkout that has not run it yet must still be able to import this file —
|
|
15
|
+
* the LSP tests skip on exactly that condition.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createRequire } from "module";
|
|
19
|
+
import { LexiconIndex, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
|
|
20
|
+
import { LEXICON_JSON_FILENAME } from "../codegen/generate";
|
|
21
|
+
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
|
|
24
|
+
/** One `lexicon-cedar.json` entry, as {@link generateRegistry} writes it. */
|
|
25
|
+
export interface CedarRegistryEntry {
|
|
26
|
+
resourceType: string;
|
|
27
|
+
kind: "resource" | "property";
|
|
28
|
+
lexicon: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
/** Cedar namespace the declaration came from, e.g. `App`. */
|
|
31
|
+
namespace?: string;
|
|
32
|
+
/** Entity types this one is `in`, for an entity declaration. */
|
|
33
|
+
memberOfTypes?: string[];
|
|
34
|
+
/** `appliesTo.principal`, for an action declaration. */
|
|
35
|
+
principalTypes?: string[];
|
|
36
|
+
/** `appliesTo.resource`, for an action declaration. */
|
|
37
|
+
resourceTypes?: string[];
|
|
38
|
+
enumValues?: string[];
|
|
39
|
+
properties?: Record<string, { type: string; required: boolean }>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type CedarRegistry = Record<string, CedarRegistryEntry>;
|
|
43
|
+
|
|
44
|
+
let cachedRegistry: CedarRegistry | null = null;
|
|
45
|
+
let cachedIndex: LexiconIndex | null = null;
|
|
46
|
+
|
|
47
|
+
/** The raw registry, for the hover formatter's Cedar-specific fields. */
|
|
48
|
+
export function cedarRegistry(): CedarRegistry {
|
|
49
|
+
if (cachedRegistry) return cachedRegistry;
|
|
50
|
+
cachedRegistry = require(`../generated/${LEXICON_JSON_FILENAME}`) as CedarRegistry;
|
|
51
|
+
return cachedRegistry;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The same registry as a `LexiconIndex`, with the property table translated. */
|
|
55
|
+
export function cedarIndex(): LexiconIndex {
|
|
56
|
+
if (cachedIndex) return cachedIndex;
|
|
57
|
+
const entries: Record<string, LexiconEntry> = {};
|
|
58
|
+
for (const [className, entry] of Object.entries(cedarRegistry())) {
|
|
59
|
+
entries[className] = {
|
|
60
|
+
resourceType: entry.resourceType,
|
|
61
|
+
kind: entry.kind,
|
|
62
|
+
lexicon: entry.lexicon,
|
|
63
|
+
propertyConstraints: entry.properties ?? {},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
cachedIndex = new LexiconIndex(entries);
|
|
67
|
+
return cachedIndex;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Reset the caches. Tests that swap the registry on disk need this; nothing else does. */
|
|
71
|
+
export function resetCedarIndexCache(): void {
|
|
72
|
+
cachedRegistry = null;
|
|
73
|
+
cachedIndex = null;
|
|
74
|
+
}
|
package/src/mcp/index.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cedar lexicon's MCP contributions.
|
|
3
|
+
*
|
|
4
|
+
* Two of the three are the conventional pair every lexicon ships, built from
|
|
5
|
+
* core's shared helpers: `cedar:diff` and `chant://cedar/resource-catalog`.
|
|
6
|
+
* The third is the one only this lexicon can answer — see
|
|
7
|
+
* ./policy-coverage.ts.
|
|
8
|
+
*
|
|
9
|
+
* Names are written as bare verbs and single-segment URIs. Core namespaces both
|
|
10
|
+
* (`cedar:<verb>`, `chant://cedar/<path>`), and applying the prefix is
|
|
11
|
+
* idempotent, so the helpers' pre-namespaced output and the bare `coverage`
|
|
12
|
+
* below both register as exactly one well-formed identifier (chant #1341).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createDiffTool, createCatalogResource } from "@intentius/chant/lexicon-plugin-helpers";
|
|
16
|
+
import type { McpToolContribution, McpResourceContribution } from "@intentius/chant/mcp/types";
|
|
17
|
+
import { cedarSerializer } from "../serializer";
|
|
18
|
+
import { computePolicyCoverage, formatPolicyCoverage } from "./policy-coverage";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* `cedar:coverage` — which schema declarations the built policy set reaches.
|
|
22
|
+
*
|
|
23
|
+
* Builds the project the same way `chant build` does rather than reading a
|
|
24
|
+
* committed `.cedar` file, so the answer is about the policies in source, not
|
|
25
|
+
* about whatever was last written to disk.
|
|
26
|
+
*/
|
|
27
|
+
export const cedarCoverageTool: McpToolContribution = {
|
|
28
|
+
name: "coverage",
|
|
29
|
+
description:
|
|
30
|
+
"Report which Cedar schema entity types and actions the project's policy set can apply to, " +
|
|
31
|
+
"which are reachable only from a forbid, and which no policy covers at all.",
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: "object" as const,
|
|
34
|
+
properties: {
|
|
35
|
+
path: {
|
|
36
|
+
type: "string",
|
|
37
|
+
description: "Path to the infrastructure project directory (defaults to the current directory)",
|
|
38
|
+
},
|
|
39
|
+
format: {
|
|
40
|
+
type: "string",
|
|
41
|
+
enum: ["json", "text"],
|
|
42
|
+
description: "Return the full report as JSON (default) or a human-readable summary",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
async handler(params: Record<string, unknown>): Promise<unknown> {
|
|
47
|
+
const { join } = await import("path");
|
|
48
|
+
const { existsSync } = await import("fs");
|
|
49
|
+
const { build } = await import("@intentius/chant/build");
|
|
50
|
+
const { loadCedarConfig } = await import("../config");
|
|
51
|
+
|
|
52
|
+
const projectRoot = (params.path as string) ?? ".";
|
|
53
|
+
const srcDir = existsSync(join(projectRoot, "src")) ? join(projectRoot, "src") : projectRoot;
|
|
54
|
+
|
|
55
|
+
const result = await build(srcDir, [cedarSerializer]);
|
|
56
|
+
if (result.errors.length > 0) {
|
|
57
|
+
return { error: "build failed", details: result.errors.map((e) => String(e)) };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const output = result.outputs.get("cedar");
|
|
61
|
+
const policySetText = typeof output === "string" ? output : (output?.primary ?? "");
|
|
62
|
+
|
|
63
|
+
const report = computePolicyCoverage({
|
|
64
|
+
policySetText,
|
|
65
|
+
projectRoot,
|
|
66
|
+
config: await loadCedarConfig(projectRoot),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return params.format === "text" ? formatPolicyCoverage(report) : report;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** Every MCP tool this lexicon contributes. */
|
|
74
|
+
export function cedarMcpTools(): McpToolContribution[] {
|
|
75
|
+
return [
|
|
76
|
+
createDiffTool(
|
|
77
|
+
cedarSerializer,
|
|
78
|
+
"Compare the current Cedar policy set against the previous build's output",
|
|
79
|
+
"cedar",
|
|
80
|
+
),
|
|
81
|
+
cedarCoverageTool,
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Every MCP resource this lexicon contributes.
|
|
87
|
+
*
|
|
88
|
+
* `createCatalogResource` locates the registry relative to the module URL it is
|
|
89
|
+
* given, resolving `<dir>/generated/lexicon-cedar.json`. That has to be
|
|
90
|
+
* `src/`, not `src/mcp/`, so the caller in plugin.ts passes its own
|
|
91
|
+
* `import.meta.url` rather than this file deriving one by walking up.
|
|
92
|
+
*/
|
|
93
|
+
export function cedarMcpResources(pluginUrl: string): McpResourceContribution[] {
|
|
94
|
+
return [
|
|
95
|
+
createCatalogResource(
|
|
96
|
+
pluginUrl,
|
|
97
|
+
"Cedar Resource Catalog",
|
|
98
|
+
"Every declaration generated from the project's Cedar schema — entity types, actions, and the Policy class",
|
|
99
|
+
"lexicon-cedar.json",
|
|
100
|
+
"cedar",
|
|
101
|
+
),
|
|
102
|
+
];
|
|
103
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { computePolicyCoverage, formatPolicyCoverage } from "./policy-coverage";
|
|
3
|
+
import { cedarMcpResources, cedarMcpTools } from "./index";
|
|
4
|
+
import { mcpNameViolations } from "@intentius/chant/cli/commands/check-lexicon-mcp";
|
|
5
|
+
|
|
6
|
+
// No projectRoot and no config, so every call resolves the bundled default
|
|
7
|
+
// schema — the same one the examples build against.
|
|
8
|
+
const against = (policySetText: string) => computePolicyCoverage({ policySetText, projectRoot: "/nonexistent" });
|
|
9
|
+
|
|
10
|
+
const ownerRead = `@id("owner-read")
|
|
11
|
+
permit (
|
|
12
|
+
principal is App::User,
|
|
13
|
+
action == App::Action::"read",
|
|
14
|
+
resource is App::Document
|
|
15
|
+
)
|
|
16
|
+
when { resource.owner == principal };`;
|
|
17
|
+
|
|
18
|
+
const secretFloor = `@id("secret-floor")
|
|
19
|
+
forbid (
|
|
20
|
+
principal,
|
|
21
|
+
action,
|
|
22
|
+
resource is App::Folder
|
|
23
|
+
)
|
|
24
|
+
when { resource.name == "secrets" };`;
|
|
25
|
+
|
|
26
|
+
describe("computePolicyCoverage", () => {
|
|
27
|
+
it("reports nothing covered for an empty policy set", () => {
|
|
28
|
+
const report = against("");
|
|
29
|
+
|
|
30
|
+
expect(report.policyCount).toBe(0);
|
|
31
|
+
expect(report.entityTypesCovered).toBe(0);
|
|
32
|
+
expect(report.actionsCovered).toBe(0);
|
|
33
|
+
expect(report.uncovered).toContain("App::Document");
|
|
34
|
+
expect(report.uncovered).toContain('App::Action::"read"');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("counts the declarations one policy can apply to", () => {
|
|
38
|
+
const report = against(ownerRead);
|
|
39
|
+
|
|
40
|
+
expect(report.policyCount).toBe(1);
|
|
41
|
+
const document = report.items.find((i) => i.name === "App::Document");
|
|
42
|
+
expect(document?.policies).toBe(1);
|
|
43
|
+
expect(document?.policyIds).toEqual(["owner-read"]);
|
|
44
|
+
expect(document?.permitted).toBe(true);
|
|
45
|
+
expect(document?.forbidden).toBe(false);
|
|
46
|
+
|
|
47
|
+
// `read` also applies to Folder in the schema, but this policy's resource
|
|
48
|
+
// scope excludes it — the resolver, not the scope text, is what decides.
|
|
49
|
+
expect(report.items.find((i) => i.name === "App::Folder")?.policies).toBe(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("resolves an action group rather than reading the scope literally", () => {
|
|
53
|
+
const report = against(ownerRead);
|
|
54
|
+
|
|
55
|
+
expect(report.items.find((i) => i.name === 'App::Action::"read"')?.policies).toBe(1);
|
|
56
|
+
expect(report.items.find((i) => i.name === 'App::Action::"write"')?.policies).toBe(0);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("flags a declaration reachable only from a forbid", () => {
|
|
60
|
+
const report = against(secretFloor);
|
|
61
|
+
|
|
62
|
+
expect(report.forbidOnly).toContain("App::Folder");
|
|
63
|
+
expect(report.items.find((i) => i.name === "App::Folder")?.forbidden).toBe(true);
|
|
64
|
+
expect(report.uncovered).not.toContain("App::Folder");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("stops flagging it once a permit reaches it too", () => {
|
|
68
|
+
const report = against(`${ownerRead}\n\n${secretFloor}`);
|
|
69
|
+
|
|
70
|
+
expect(report.policyCount).toBe(2);
|
|
71
|
+
expect(report.forbidOnly).toContain("App::Folder");
|
|
72
|
+
expect(report.forbidOnly).not.toContain("App::Document");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("names a bare permit as covering every action and every reachable entity type", () => {
|
|
76
|
+
const report = against("permit (principal, action, resource);");
|
|
77
|
+
|
|
78
|
+
expect(report.actionsCovered).toBe(report.actions);
|
|
79
|
+
|
|
80
|
+
// Group and Team appear in no action's `appliesTo` — they exist only as
|
|
81
|
+
// containers to be `in`. Not even `permit (principal, action, resource)`
|
|
82
|
+
// reaches them, which is the resolver telling the truth: no request can
|
|
83
|
+
// ever name them as a principal or a resource. Reporting them as covered
|
|
84
|
+
// would be the more comfortable answer and the wrong one.
|
|
85
|
+
expect(report.uncovered).toEqual(["App::Group", "App::Team"]);
|
|
86
|
+
expect(report.entityTypesCovered).toBe(report.entityTypes - 2);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("reads the effect from Cedar's parse, not from the surrounding text", () => {
|
|
90
|
+
// A permit whose annotation contains the word `forbid` at the start of a
|
|
91
|
+
// line, and a policy id containing an escaped quote. Both defeated the
|
|
92
|
+
// text-scanning version of this.
|
|
93
|
+
const report = against(`@id("say \\"forbid\\" here")
|
|
94
|
+
@note("
|
|
95
|
+
forbid (this is prose)")
|
|
96
|
+
permit (
|
|
97
|
+
principal is App::User,
|
|
98
|
+
action == App::Action::"read",
|
|
99
|
+
resource is App::Document
|
|
100
|
+
);`);
|
|
101
|
+
|
|
102
|
+
const document = report.items.find((i) => i.name === "App::Document");
|
|
103
|
+
expect(document?.permitted).toBe(true);
|
|
104
|
+
expect(document?.forbidden).toBe(false);
|
|
105
|
+
expect(document?.policyIds).toEqual(['say "forbid" here']);
|
|
106
|
+
expect(report.forbidOnly).not.toContain("App::Document");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("falls back to a positional id when a policy carries no @id", () => {
|
|
110
|
+
const report = against("permit (principal, action, resource);");
|
|
111
|
+
|
|
112
|
+
expect(report.items.find((i) => i.name === "App::Document")?.policyIds).toEqual(["policy-1"]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("distinguishes a policy set that will not parse from one with no policies", () => {
|
|
116
|
+
const report = against("permit (principal, action");
|
|
117
|
+
|
|
118
|
+
expect(report.parseErrors.length).toBeGreaterThan(0);
|
|
119
|
+
expect(report.policyCount).toBe(0);
|
|
120
|
+
// Same counts as an empty set — the parseErrors field is the only thing
|
|
121
|
+
// that tells the two apart, which is why it exists.
|
|
122
|
+
expect(report.uncovered).toContain("App::Document");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("names a policy that can never fire", () => {
|
|
126
|
+
// App::Ghost parses — it is a syntactically valid entity type — and the
|
|
127
|
+
// resolver answers "success, nothing" rather than failing. A permit no
|
|
128
|
+
// request can ever match is almost always a typo'd entity type, so the
|
|
129
|
+
// empty envelope is reported rather than swallowed.
|
|
130
|
+
const report = against(`@id("ghost")
|
|
131
|
+
permit (
|
|
132
|
+
principal,
|
|
133
|
+
action,
|
|
134
|
+
resource is App::Ghost
|
|
135
|
+
);`);
|
|
136
|
+
|
|
137
|
+
expect(report.inert).toEqual(["ghost"]);
|
|
138
|
+
expect(report.unresolved).toEqual([]);
|
|
139
|
+
expect(report.uncovered).toContain("App::Document");
|
|
140
|
+
expect(formatPolicyCoverage(report)).toContain("Never fires: ghost");
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("formatPolicyCoverage", () => {
|
|
145
|
+
it("summarises counts, uncovered declarations and forbid-only ones", () => {
|
|
146
|
+
const text = formatPolicyCoverage(against(`${ownerRead}\n\n${secretFloor}`));
|
|
147
|
+
|
|
148
|
+
expect(text).toContain("2 policy/policies");
|
|
149
|
+
expect(text).toContain("Entity types:");
|
|
150
|
+
expect(text).toContain("Uncovered:");
|
|
151
|
+
expect(text).toMatch(/Forbid only:.*App::Folder/);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("MCP contributions", () => {
|
|
156
|
+
it("registers three contributions under one well-formed cedar namespace", () => {
|
|
157
|
+
const tools = cedarMcpTools();
|
|
158
|
+
const resources = cedarMcpResources(import.meta.url);
|
|
159
|
+
|
|
160
|
+
expect(tools).toHaveLength(2);
|
|
161
|
+
expect(resources).toHaveLength(1);
|
|
162
|
+
expect(mcpNameViolations("cedar", tools, resources)).toEqual([]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("ships the conventional diff tool and catalog resource beside the cedar-specific one", () => {
|
|
166
|
+
expect(cedarMcpTools().map((t) => t.name)).toEqual(["cedar:diff", "coverage"]);
|
|
167
|
+
expect(cedarMcpResources(import.meta.url).map((r) => r.uri)).toEqual(["cedar:resource-catalog"]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("declares an input schema the agent can fill in", () => {
|
|
171
|
+
const coverage = cedarMcpTools().find((t) => t.name === "coverage");
|
|
172
|
+
|
|
173
|
+
expect(coverage?.inputSchema.properties).toHaveProperty("path");
|
|
174
|
+
expect(coverage?.inputSchema.properties).toHaveProperty("format");
|
|
175
|
+
});
|
|
176
|
+
});
|