cedar-mcp-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/.editorconfig +12 -0
  2. package/.github/workflows/ci.yml +31 -0
  3. package/.github/workflows/release.yml +42 -0
  4. package/.nvmrc +1 -0
  5. package/CHANGELOG.md +241 -0
  6. package/CONTRIBUTING.md +83 -0
  7. package/LICENSE +182 -0
  8. package/README.md +1635 -0
  9. package/SECURITY.md +37 -0
  10. package/dist/http-server.d.ts +61 -0
  11. package/dist/http-server.d.ts.map +1 -0
  12. package/dist/http-server.js +194 -0
  13. package/dist/http-server.js.map +1 -0
  14. package/dist/index.d.ts +32 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +270 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/parser/policy-ast.d.ts +49 -0
  19. package/dist/parser/policy-ast.d.ts.map +1 -0
  20. package/dist/parser/policy-ast.js +311 -0
  21. package/dist/parser/policy-ast.js.map +1 -0
  22. package/dist/prompts/index.d.ts +38 -0
  23. package/dist/prompts/index.d.ts.map +1 -0
  24. package/dist/prompts/index.js +172 -0
  25. package/dist/prompts/index.js.map +1 -0
  26. package/dist/resources/ref-resolver.d.ts +23 -0
  27. package/dist/resources/ref-resolver.d.ts.map +1 -0
  28. package/dist/resources/ref-resolver.js +128 -0
  29. package/dist/resources/ref-resolver.js.map +1 -0
  30. package/dist/resources/store-manager.d.ts +64 -0
  31. package/dist/resources/store-manager.d.ts.map +1 -0
  32. package/dist/resources/store-manager.js +221 -0
  33. package/dist/resources/store-manager.js.map +1 -0
  34. package/dist/server.d.ts +18 -0
  35. package/dist/server.d.ts.map +1 -0
  36. package/dist/server.js +539 -0
  37. package/dist/server.js.map +1 -0
  38. package/dist/tools/advise/avp-rules.d.ts +49 -0
  39. package/dist/tools/advise/avp-rules.d.ts.map +1 -0
  40. package/dist/tools/advise/avp-rules.js +59 -0
  41. package/dist/tools/advise/avp-rules.js.map +1 -0
  42. package/dist/tools/advise/cedar-patterns.d.ts +24 -0
  43. package/dist/tools/advise/cedar-patterns.d.ts.map +1 -0
  44. package/dist/tools/advise/cedar-patterns.js +57 -0
  45. package/dist/tools/advise/cedar-patterns.js.map +1 -0
  46. package/dist/tools/advise/context-builder.d.ts +28 -0
  47. package/dist/tools/advise/context-builder.d.ts.map +1 -0
  48. package/dist/tools/advise/context-builder.js +89 -0
  49. package/dist/tools/advise/context-builder.js.map +1 -0
  50. package/dist/tools/advise/gotchas.d.ts +15 -0
  51. package/dist/tools/advise/gotchas.d.ts.map +1 -0
  52. package/dist/tools/advise/gotchas.js +83 -0
  53. package/dist/tools/advise/gotchas.js.map +1 -0
  54. package/dist/tools/advise.d.ts +96 -0
  55. package/dist/tools/advise.d.ts.map +1 -0
  56. package/dist/tools/advise.js +258 -0
  57. package/dist/tools/advise.js.map +1 -0
  58. package/dist/tools/authorize-batch.d.ts +35 -0
  59. package/dist/tools/authorize-batch.d.ts.map +1 -0
  60. package/dist/tools/authorize-batch.js +262 -0
  61. package/dist/tools/authorize-batch.js.map +1 -0
  62. package/dist/tools/authorize.d.ts +115 -0
  63. package/dist/tools/authorize.d.ts.map +1 -0
  64. package/dist/tools/authorize.js +373 -0
  65. package/dist/tools/authorize.js.map +1 -0
  66. package/dist/tools/check-change.d.ts +19 -0
  67. package/dist/tools/check-change.d.ts.map +1 -0
  68. package/dist/tools/check-change.js +91 -0
  69. package/dist/tools/check-change.js.map +1 -0
  70. package/dist/tools/diff-schema.d.ts +103 -0
  71. package/dist/tools/diff-schema.d.ts.map +1 -0
  72. package/dist/tools/diff-schema.js +379 -0
  73. package/dist/tools/diff-schema.js.map +1 -0
  74. package/dist/tools/diff-stores.d.ts +45 -0
  75. package/dist/tools/diff-stores.d.ts.map +1 -0
  76. package/dist/tools/diff-stores.js +222 -0
  77. package/dist/tools/diff-stores.js.map +1 -0
  78. package/dist/tools/explain.d.ts +80 -0
  79. package/dist/tools/explain.d.ts.map +1 -0
  80. package/dist/tools/explain.js +187 -0
  81. package/dist/tools/explain.js.map +1 -0
  82. package/dist/tools/format.d.ts +11 -0
  83. package/dist/tools/format.d.ts.map +1 -0
  84. package/dist/tools/format.js +20 -0
  85. package/dist/tools/format.js.map +1 -0
  86. package/dist/tools/generate-sample.d.ts +28 -0
  87. package/dist/tools/generate-sample.d.ts.map +1 -0
  88. package/dist/tools/generate-sample.js +568 -0
  89. package/dist/tools/generate-sample.js.map +1 -0
  90. package/dist/tools/link-template.d.ts +17 -0
  91. package/dist/tools/link-template.d.ts.map +1 -0
  92. package/dist/tools/link-template.js +78 -0
  93. package/dist/tools/link-template.js.map +1 -0
  94. package/dist/tools/list-template-links.d.ts +16 -0
  95. package/dist/tools/list-template-links.d.ts.map +1 -0
  96. package/dist/tools/list-template-links.js +22 -0
  97. package/dist/tools/list-template-links.js.map +1 -0
  98. package/dist/tools/list-templates.d.ts +16 -0
  99. package/dist/tools/list-templates.d.ts.map +1 -0
  100. package/dist/tools/list-templates.js +36 -0
  101. package/dist/tools/list-templates.js.map +1 -0
  102. package/dist/tools/translate.d.ts +11 -0
  103. package/dist/tools/translate.d.ts.map +1 -0
  104. package/dist/tools/translate.js +53 -0
  105. package/dist/tools/translate.js.map +1 -0
  106. package/dist/tools/validate-entities.d.ts +19 -0
  107. package/dist/tools/validate-entities.d.ts.map +1 -0
  108. package/dist/tools/validate-entities.js +88 -0
  109. package/dist/tools/validate-entities.js.map +1 -0
  110. package/dist/tools/validate-schema.d.ts +22 -0
  111. package/dist/tools/validate-schema.d.ts.map +1 -0
  112. package/dist/tools/validate-schema.js +89 -0
  113. package/dist/tools/validate-schema.js.map +1 -0
  114. package/dist/tools/validate-template.d.ts +18 -0
  115. package/dist/tools/validate-template.d.ts.map +1 -0
  116. package/dist/tools/validate-template.js +59 -0
  117. package/dist/tools/validate-template.js.map +1 -0
  118. package/dist/tools/validate.d.ts +90 -0
  119. package/dist/tools/validate.d.ts.map +1 -0
  120. package/dist/tools/validate.js +351 -0
  121. package/dist/tools/validate.js.map +1 -0
  122. package/dist/utils/format-detector.d.ts +49 -0
  123. package/dist/utils/format-detector.d.ts.map +1 -0
  124. package/dist/utils/format-detector.js +298 -0
  125. package/dist/utils/format-detector.js.map +1 -0
  126. package/examples/README.md +36 -0
  127. package/examples/abac-multi-tenant/README.md +150 -0
  128. package/examples/abac-multi-tenant/entities/users-and-docs.json +33 -0
  129. package/examples/abac-multi-tenant/policies/member-read-internal.cedar +9 -0
  130. package/examples/abac-multi-tenant/policies/owner-full-access.cedar +9 -0
  131. package/examples/abac-multi-tenant/policies/premium-share-guard.cedar +9 -0
  132. package/examples/abac-multi-tenant/policies/private-doc-guard.cedar +13 -0
  133. package/examples/abac-multi-tenant/run.ts +92 -0
  134. package/examples/abac-multi-tenant/schema.json +60 -0
  135. package/examples/api-gateway-path-routing/README.md +154 -0
  136. package/examples/api-gateway-path-routing/entities/users-and-roles.json +20 -0
  137. package/examples/api-gateway-path-routing/policies/admin-full-access.cedar +6 -0
  138. package/examples/api-gateway-path-routing/policies/developer-projects.cedar +14 -0
  139. package/examples/api-gateway-path-routing/policies/viewer-readonly.cedar +10 -0
  140. package/examples/api-gateway-path-routing/run.ts +108 -0
  141. package/examples/api-gateway-path-routing/schema.json +54 -0
  142. package/examples/rbac-document-management/README.md +167 -0
  143. package/examples/rbac-document-management/entities/users-and-docs.json +43 -0
  144. package/examples/rbac-document-management/policies/admin.cedar +6 -0
  145. package/examples/rbac-document-management/policies/editor.cedar +6 -0
  146. package/examples/rbac-document-management/policies/top-secret-forbid.cedar +13 -0
  147. package/examples/rbac-document-management/policies/viewer.cedar +6 -0
  148. package/examples/rbac-document-management/run.ts +87 -0
  149. package/examples/rbac-document-management/schema.json +57 -0
  150. package/package.json +50 -0
  151. package/src/http-server.ts +239 -0
  152. package/src/index.ts +294 -0
  153. package/src/parser/policy-ast.ts +345 -0
  154. package/src/prompts/README.md +3 -0
  155. package/src/prompts/index.ts +217 -0
  156. package/src/resources/ref-resolver.ts +134 -0
  157. package/src/resources/store-manager.ts +248 -0
  158. package/src/server.ts +711 -0
  159. package/src/tools/advise/avp-rules.ts +70 -0
  160. package/src/tools/advise/cedar-patterns.ts +73 -0
  161. package/src/tools/advise/context-builder.ts +109 -0
  162. package/src/tools/advise/gotchas.ts +92 -0
  163. package/src/tools/advise.ts +366 -0
  164. package/src/tools/authorize-batch.ts +345 -0
  165. package/src/tools/authorize.ts +464 -0
  166. package/src/tools/check-change.ts +119 -0
  167. package/src/tools/diff-schema.ts +510 -0
  168. package/src/tools/diff-stores.ts +298 -0
  169. package/src/tools/explain.ts +278 -0
  170. package/src/tools/format.ts +33 -0
  171. package/src/tools/generate-sample.ts +665 -0
  172. package/src/tools/link-template.ts +109 -0
  173. package/src/tools/list-template-links.ts +41 -0
  174. package/src/tools/list-templates.ts +55 -0
  175. package/src/tools/translate.ts +66 -0
  176. package/src/tools/validate-entities.ts +125 -0
  177. package/src/tools/validate-schema.ts +128 -0
  178. package/src/tools/validate-template.ts +72 -0
  179. package/src/tools/validate.ts +459 -0
  180. package/src/utils/format-detector.ts +356 -0
  181. package/test/fixtures/docmgmt.ts +121 -0
  182. package/test/fixtures/multitenant.ts +163 -0
  183. package/test/index.test.ts +96 -0
  184. package/test/integration/e2e/behavior.test.ts +359 -0
  185. package/test/integration/e2e/edge-cases.test.ts +365 -0
  186. package/test/integration/e2e/failure-modes.test.ts +266 -0
  187. package/test/integration/e2e/protocol.test.ts +252 -0
  188. package/test/integration/http-smoke.test.ts +588 -0
  189. package/test/integration/smoke.test.ts +475 -0
  190. package/test/prompts/prompts.test.ts +173 -0
  191. package/test/property/properties.test.ts +234 -0
  192. package/test/resources/ref-resolver.test.ts +186 -0
  193. package/test/resources/store-manager.test.ts +344 -0
  194. package/test/setup.test.ts +7 -0
  195. package/test/tools/advise/avp-rules.test.ts +76 -0
  196. package/test/tools/advise.test.ts +339 -0
  197. package/test/tools/authorize-batch.test.ts +459 -0
  198. package/test/tools/authorize.test.ts +682 -0
  199. package/test/tools/check-change.test.ts +104 -0
  200. package/test/tools/cross-fixture.test.ts +170 -0
  201. package/test/tools/diff-schema.test.ts +355 -0
  202. package/test/tools/diff-stores.test.ts +291 -0
  203. package/test/tools/explain.test.ts +221 -0
  204. package/test/tools/format.test.ts +33 -0
  205. package/test/tools/generate-sample.test.ts +480 -0
  206. package/test/tools/link-template.test.ts +90 -0
  207. package/test/tools/list-templates.test.ts +151 -0
  208. package/test/tools/translate.test.ts +89 -0
  209. package/test/tools/validate-entities.test.ts +178 -0
  210. package/test/tools/validate-schema.test.ts +86 -0
  211. package/test/tools/validate-template.test.ts +89 -0
  212. package/test/tools/validate.test.ts +331 -0
  213. package/test/utils/format-detector.test.ts +518 -0
  214. package/tsconfig.json +17 -0
  215. package/vitest.config.ts +13 -0
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Unit tests for the stdio entry-point helpers in src/index.ts. Tests that
3
+ * need a real spawned process live in test/integration/smoke.test.ts
4
+ * (S6, S6b, S6c). These exercise the helpers directly.
5
+ */
6
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
7
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { basename, join } from "node:path";
10
+ import { populateCwdFallback } from "../src/index.js";
11
+ import { storeManager } from "../src/resources/store-manager.js";
12
+
13
+ describe("populateCwdFallback — kickoff-12 sync cwd-fallback (Round 5 Scenario E fix)", () => {
14
+ const tempDirs: string[] = [];
15
+
16
+ beforeEach(() => {
17
+ // Reset the singleton so a polluted state from a prior test or a
18
+ // file-parallel sibling cannot mask a real bug here. 12b audit
19
+ // Finding 3 (test fragility).
20
+ storeManager.loadFromRoots([]);
21
+ });
22
+
23
+ afterEach(() => {
24
+ storeManager.loadFromRoots([]);
25
+ while (tempDirs.length > 0) {
26
+ const dir = tempDirs.pop()!;
27
+ rmSync(dir, { recursive: true, force: true });
28
+ }
29
+ });
30
+
31
+ it("returns a root descriptor and loads the store when cwd has schema.cedarschema", () => {
32
+ const dir = mkdtempSync(join(tmpdir(), "cedar-sync-schema-"));
33
+ tempDirs.push(dir);
34
+ writeFileSync(join(dir, "schema.cedarschema"), `namespace DocMgmt {}`);
35
+
36
+ const loaded = populateCwdFallback(dir);
37
+
38
+ expect(loaded).toEqual({ uri: `file://${dir}`, name: basename(dir) });
39
+ expect(storeManager.listStoreNames()).toEqual([basename(dir)]);
40
+ });
41
+
42
+ it("returns a root descriptor and loads the store when cwd has schema.json", () => {
43
+ const dir = mkdtempSync(join(tmpdir(), "cedar-sync-json-"));
44
+ tempDirs.push(dir);
45
+ writeFileSync(join(dir, "schema.json"), `{"DocMgmt":{}}`);
46
+
47
+ expect(populateCwdFallback(dir)).not.toBeNull();
48
+ expect(storeManager.listStoreNames()).toEqual([basename(dir)]);
49
+ });
50
+
51
+ it("returns a root descriptor and loads the store when cwd has only a policies/ directory (no schema)", () => {
52
+ // This matches the 11d-audit Probe B scenario. populateCwdFallback fires;
53
+ // cedar_advise's downstream auto-resolve then handles the schemaless case
54
+ // via the `not_provided` degrade path landed in commit 0f35740.
55
+ const dir = mkdtempSync(join(tmpdir(), "cedar-sync-policiesonly-"));
56
+ tempDirs.push(dir);
57
+ mkdirSync(join(dir, "policies"));
58
+
59
+ expect(populateCwdFallback(dir)).not.toBeNull();
60
+ expect(storeManager.listStoreNames()).toEqual([basename(dir)]);
61
+ });
62
+
63
+ it("returns null and leaves StoreManager untouched when cwd does not look like a Cedar workspace", () => {
64
+ const dir = mkdtempSync(join(tmpdir(), "cedar-sync-empty-"));
65
+ tempDirs.push(dir);
66
+ // Empty directory — no schema.cedarschema, no schema.json, no policies/.
67
+
68
+ expect(populateCwdFallback(dir)).toBeNull();
69
+ expect(storeManager.listStoreNames()).toEqual([]);
70
+ });
71
+
72
+ it("populates StoreManager synchronously (caller can observe state immediately on the next line)", () => {
73
+ // The whole point of 12a: the call returns AFTER state is mutated, with
74
+ // no async gap. A caller reading listStoreNames() on the next statement
75
+ // must see the populated store. If populateCwdFallback ever becomes
76
+ // async, every caller that relies on this property breaks.
77
+ const dir = mkdtempSync(join(tmpdir(), "cedar-sync-immediate-"));
78
+ tempDirs.push(dir);
79
+ writeFileSync(join(dir, "schema.cedarschema"), `namespace DocMgmt {}`);
80
+
81
+ populateCwdFallback(dir);
82
+ // Immediate read — no awaits, no microtask boundary.
83
+ const names = storeManager.listStoreNames();
84
+
85
+ expect(names).toEqual([basename(dir)]);
86
+ });
87
+
88
+ it("returns null when cwd is the filesystem root (12b audit Finding 2 — sandbox-bypass guard)", () => {
89
+ // cwd === "/" → store.path would normalize to "" → isPathAllowed would
90
+ // return true for every filesystem path (because `"<anything>".startsWith("")`
91
+ // is true). populateCwdFallback rejects this case explicitly; StoreManager
92
+ // also rejects empty-rawPath roots as a second defense layer.
93
+ expect(populateCwdFallback("/")).toBeNull();
94
+ expect(storeManager.listStoreNames()).toEqual([]);
95
+ });
96
+ });
@@ -0,0 +1,359 @@
1
+ /**
2
+ * E2E layer 2: cross-tool behavior invariants.
3
+ *
4
+ * Invariants that span multiple tools end-to-end through the MCP protocol.
5
+ * Each test asserts a property the tool surface MUST satisfy, not a single
6
+ * happy-path output. If two tools disagree on the same input, the invariant
7
+ * surfaces it.
8
+ *
9
+ * Transport: stdio (same rationale as protocol.test.ts).
10
+ *
11
+ * Run: npx vitest run test/integration/e2e/behavior
12
+ */
13
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
14
+ import { join } from "node:path";
15
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
16
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
17
+
18
+ const repoRoot = join(import.meta.dirname, "../../..");
19
+
20
+ function makeStdioClient(): { client: Client; transport: StdioClientTransport } {
21
+ const transport = new StdioClientTransport({
22
+ command: "npx",
23
+ args: ["tsx", "src/index.ts"],
24
+ cwd: repoRoot,
25
+ stderr: "pipe",
26
+ });
27
+ const client = new Client(
28
+ { name: "e2e-behavior", version: "1.0.0" },
29
+ { capabilities: {} }
30
+ );
31
+ return { client, transport };
32
+ }
33
+
34
+ function parseToolResult(result: unknown): unknown {
35
+ const r = result as { content?: Array<{ type: string; text?: string }> };
36
+ const textBlock = r.content?.find((b) => b.type === "text");
37
+ if (!textBlock?.text) throw new Error("No text content in tool result");
38
+ return JSON.parse(textBlock.text);
39
+ }
40
+
41
+ const SCHEMA_JSON = JSON.stringify({
42
+ DocMgmt: {
43
+ entityTypes: {
44
+ User: {
45
+ memberOfTypes: ["Role"],
46
+ shape: {
47
+ type: "Record",
48
+ attributes: {
49
+ name: { type: "String", required: true },
50
+ email: { type: "String", required: true },
51
+ },
52
+ },
53
+ },
54
+ Role: { memberOfTypes: [], shape: { type: "Record", attributes: {} } },
55
+ Document: {
56
+ memberOfTypes: ["Folder"],
57
+ shape: {
58
+ type: "Record",
59
+ attributes: {
60
+ owner: { type: "String", required: true },
61
+ classification: { type: "String", required: true },
62
+ },
63
+ },
64
+ },
65
+ Folder: { memberOfTypes: [], shape: { type: "Record", attributes: {} } },
66
+ },
67
+ actions: {
68
+ read: { appliesTo: { principalTypes: ["User"], resourceTypes: ["Document"], context: { type: "Record", attributes: {} } } },
69
+ write: { appliesTo: { principalTypes: ["User"], resourceTypes: ["Document"], context: { type: "Record", attributes: {} } } },
70
+ },
71
+ },
72
+ });
73
+
74
+ const ADMIN_POLICY = `permit (
75
+ principal in DocMgmt::Role::"admin",
76
+ action,
77
+ resource
78
+ );`;
79
+
80
+ const VIEWER_POLICY = `permit (
81
+ principal in DocMgmt::Role::"viewer",
82
+ action == DocMgmt::Action::"read",
83
+ resource
84
+ )
85
+ when {
86
+ resource.classification == "public"
87
+ };`;
88
+
89
+ describe("e2e behavior", () => {
90
+ let client: Client | undefined;
91
+ let transport: StdioClientTransport | undefined;
92
+
93
+ beforeEach(async () => {
94
+ const conn = makeStdioClient();
95
+ client = conn.client;
96
+ transport = conn.transport;
97
+ await client.connect(transport);
98
+ });
99
+
100
+ afterEach(async () => {
101
+ try { await client?.close(); } catch { /* ignore */ }
102
+ try { await transport?.close(); } catch { /* ignore */ }
103
+ client = undefined;
104
+ transport = undefined;
105
+ });
106
+
107
+ it("B1 — translate round-trip preserves validation: text→json→text validates the same", async () => {
108
+ // Invariant: if a policy validates against a schema, translating it to JSON and
109
+ // back to text should produce a policy that ALSO validates against the same schema.
110
+ // Failure case: AST round-trip drops a condition clause, validation passes "before"
111
+ // but fails "after" (or vice versa). Catches lossy translations.
112
+
113
+ const original = ADMIN_POLICY;
114
+
115
+ const toJsonRaw = await client!.callTool({
116
+ name: "cedar_translate",
117
+ arguments: { input: original, type: "policy", direction: "to_json" },
118
+ });
119
+ const toJson = parseToolResult(toJsonRaw) as { output: string; error: null };
120
+ expect(toJson.error).toBeNull();
121
+
122
+ const toCedarRaw = await client!.callTool({
123
+ name: "cedar_translate",
124
+ arguments: { input: toJson.output, type: "policy", direction: "to_cedar" },
125
+ });
126
+ const toCedar = parseToolResult(toCedarRaw) as { output: string; error: null };
127
+ expect(toCedar.error).toBeNull();
128
+
129
+ const validateOriginal = parseToolResult(
130
+ await client!.callTool({ name: "cedar_validate", arguments: { policies: original, schema: SCHEMA_JSON } })
131
+ ) as { valid: boolean };
132
+ const validateRoundtrip = parseToolResult(
133
+ await client!.callTool({ name: "cedar_validate", arguments: { policies: toCedar.output, schema: SCHEMA_JSON } })
134
+ ) as { valid: boolean };
135
+
136
+ expect(validateOriginal.valid).toBe(validateRoundtrip.valid);
137
+ expect(validateOriginal.valid).toBe(true);
138
+ }, 30_000);
139
+
140
+ it("B2 — format idempotency: format(format(P)) === format(P)", async () => {
141
+ // Invariant: formatting a policy twice produces the same output as formatting once.
142
+ // Failure case: an inconsistent formatter that adds/removes whitespace differently
143
+ // on successive runs would break diff tools and CI checks.
144
+ const firstRaw = await client!.callTool({ name: "cedar_format", arguments: { policies: VIEWER_POLICY } });
145
+ const first = parseToolResult(firstRaw) as { formatted: string };
146
+
147
+ const secondRaw = await client!.callTool({ name: "cedar_format", arguments: { policies: first.formatted } });
148
+ const second = parseToolResult(secondRaw) as { formatted: string };
149
+
150
+ expect(second.formatted).toBe(first.formatted);
151
+ }, 30_000);
152
+
153
+ it("B3 — generate-then-authorize agrees: when generator claims Allow, cedar_authorize confirms Allow", async () => {
154
+ // Invariant: if cedar_generate_sample_request claims ready_to_test:true and
155
+ // decision:"Allow", then cedar_authorize on the same request MUST also return Allow.
156
+ // If the generator is unable to construct a satisfying request (ready_to_test:false),
157
+ // that's a known limitation we don't fail on — but if it CLAIMS success and the
158
+ // independent authorize disagrees, that's a real product bug.
159
+ const genRaw = await client!.callTool({
160
+ name: "cedar_generate_sample_request",
161
+ arguments: { policy: ADMIN_POLICY, schema: SCHEMA_JSON, target_decision: "allow" },
162
+ });
163
+ const gen = parseToolResult(genRaw) as {
164
+ principal: string;
165
+ action: string;
166
+ resource: string;
167
+ entities: unknown[];
168
+ decision?: "Allow" | "Deny";
169
+ ready_to_test?: boolean;
170
+ };
171
+
172
+ if (gen.ready_to_test === false || gen.decision !== "Allow") {
173
+ // eslint-disable-next-line no-console
174
+ console.warn("B3 — generator did not claim success for target=allow (ready_to_test=" + gen.ready_to_test + ", decision=" + gen.decision + "); skipping cross-tool assertion.");
175
+ return;
176
+ }
177
+
178
+ const authRaw = await client!.callTool({
179
+ name: "cedar_authorize",
180
+ arguments: {
181
+ policies: ADMIN_POLICY,
182
+ principal: gen.principal,
183
+ action: gen.action,
184
+ resource: gen.resource,
185
+ entities: JSON.stringify(gen.entities),
186
+ schema: SCHEMA_JSON,
187
+ },
188
+ });
189
+ const auth = parseToolResult(authRaw) as { decision: string };
190
+ expect(auth.decision).toBe("Allow");
191
+ }, 30_000);
192
+
193
+ // B4 exercises the skip path: the generator self-reports ready_to_test:false +
194
+ // decision:"Allow" for target=deny on the ADMIN policy (which permits everything for
195
+ // admin role — no satisfying deny exists for an admin principal). The skip is the
196
+ // correct behavior; the assertion is vacuously true. If the generator one day
197
+ // produces a satisfying deny request, the .skip path would not fire and we'd assert
198
+ // Deny against authorize like B3 does.
199
+ it("B4 — generate-then-authorize agrees: when generator claims Deny, cedar_authorize confirms Deny", async () => {
200
+ // Symmetric invariant for the deny path. Same skip pattern as B3 when the
201
+ // generator self-reports it couldn't produce a satisfying example.
202
+ const genRaw = await client!.callTool({
203
+ name: "cedar_generate_sample_request",
204
+ arguments: { policy: ADMIN_POLICY, schema: SCHEMA_JSON, target_decision: "deny" },
205
+ });
206
+ const gen = parseToolResult(genRaw) as {
207
+ principal: string;
208
+ action: string;
209
+ resource: string;
210
+ entities: unknown[];
211
+ decision?: "Allow" | "Deny";
212
+ ready_to_test?: boolean;
213
+ };
214
+
215
+ if (gen.ready_to_test === false || gen.decision !== "Deny") {
216
+ // eslint-disable-next-line no-console
217
+ console.warn("B4 — generator did not claim success for target=deny (ready_to_test=" + gen.ready_to_test + ", decision=" + gen.decision + "); skipping cross-tool assertion.");
218
+ return;
219
+ }
220
+
221
+ const authRaw = await client!.callTool({
222
+ name: "cedar_authorize",
223
+ arguments: {
224
+ policies: ADMIN_POLICY,
225
+ principal: gen.principal,
226
+ action: gen.action,
227
+ resource: gen.resource,
228
+ entities: JSON.stringify(gen.entities),
229
+ schema: SCHEMA_JSON,
230
+ },
231
+ });
232
+ const auth = parseToolResult(authRaw) as { decision: string };
233
+
234
+ expect(auth.decision).toBe("Deny");
235
+ }, 30_000);
236
+
237
+ it("B5 — diff_schema identity: diffing a schema against itself reports no changes, risk safe", async () => {
238
+ // Invariant: any diff function applied to two equal inputs MUST report zero changes.
239
+ // Failure case: a normalizer that introduces spurious differences (e.g., __cedar:: prefix
240
+ // stripping that's asymmetric) would flag identical schemas as different. We tested this
241
+ // explicitly in the unit suite but the e2e path adds the MCP protocol layer on top —
242
+ // a serialization bug could re-emerge here.
243
+ const raw = await client!.callTool({
244
+ name: "cedar_diff_schema",
245
+ arguments: { blue: SCHEMA_JSON, green: SCHEMA_JSON },
246
+ });
247
+ const result = parseToolResult(raw) as {
248
+ risk_level: string;
249
+ entity_types: { added: unknown[]; removed: unknown[]; modified: unknown[] };
250
+ actions: { added: unknown[]; removed: unknown[]; modified: unknown[] };
251
+ };
252
+
253
+ expect(result.risk_level).toBe("safe");
254
+ expect(result.entity_types.added).toHaveLength(0);
255
+ expect(result.entity_types.removed).toHaveLength(0);
256
+ expect(result.entity_types.modified).toHaveLength(0);
257
+ expect(result.actions.added).toHaveLength(0);
258
+ expect(result.actions.removed).toHaveLength(0);
259
+ expect(result.actions.modified).toHaveLength(0);
260
+ }, 20_000);
261
+
262
+ it("B6 — check_policy_change identity: comparing a policy against itself reports no changes", async () => {
263
+ // Invariant: cedar_check_policy_change(P, P) must say can_update_in_place: true
264
+ // with no changes. Failure case: an over-eager change detector flagging whitespace
265
+ // or comment differences as semantic changes would generate noisy AVP recommendations.
266
+ const raw = await client!.callTool({
267
+ name: "cedar_check_policy_change",
268
+ arguments: { old_policy: VIEWER_POLICY, new_policy: VIEWER_POLICY },
269
+ });
270
+ const result = parseToolResult(raw) as {
271
+ can_update_in_place: boolean;
272
+ changes: unknown[];
273
+ };
274
+
275
+ expect(result.can_update_in_place).toBe(true);
276
+ expect(result.changes).toHaveLength(0);
277
+ }, 20_000);
278
+
279
+ it("B7 — validate + authorize agree on schema-level errors", async () => {
280
+ // Invariant: if cedar_validate flags a policy as invalid against a schema (e.g.,
281
+ // references a missing attribute), then cedar_authorize with validateRequest=true
282
+ // on the same policy+schema should ALSO surface the issue (as a request-level
283
+ // error or as a Deny). The two tools must not disagree about schema validity.
284
+ const badPolicy = `permit (principal, action, resource) when { resource.nonexistent_attr == "x" };`;
285
+
286
+ const validateRaw = await client!.callTool({
287
+ name: "cedar_validate",
288
+ arguments: { policies: badPolicy, schema: SCHEMA_JSON },
289
+ });
290
+ const validate = parseToolResult(validateRaw) as { valid: boolean; errors: Array<{ message: string }> };
291
+
292
+ expect(validate.valid).toBe(false);
293
+ expect(validate.errors.length).toBeGreaterThan(0);
294
+ expect(validate.errors[0].message).toContain("nonexistent_attr");
295
+
296
+ // Now run an authorize with this same policy. Cedar's behavior: a policy that
297
+ // references a missing attribute evaluates to false (silently skipped) at
298
+ // authorize time. So the request returns Deny (no permit applies). The
299
+ // INVARIANT here is "they don't disagree about the policy being broken" —
300
+ // validate says invalid, authorize returns the default-deny. If authorize
301
+ // returned Allow for this policy, it would prove they disagree.
302
+ const authRaw = await client!.callTool({
303
+ name: "cedar_authorize",
304
+ arguments: {
305
+ policies: badPolicy,
306
+ principal: 'DocMgmt::User::"alice"',
307
+ action: 'DocMgmt::Action::"read"',
308
+ resource: 'DocMgmt::Document::"d1"',
309
+ entities: JSON.stringify([
310
+ { uid: { type: "DocMgmt::User", id: "alice" }, attrs: { name: "Alice", email: "a@b.c" }, parents: [] },
311
+ { uid: { type: "DocMgmt::Document", id: "d1" }, attrs: { owner: "alice", classification: "public" }, parents: [] },
312
+ ]),
313
+ schema: SCHEMA_JSON,
314
+ },
315
+ });
316
+ const auth = parseToolResult(authRaw) as { decision: string };
317
+
318
+ expect(auth.decision).toBe("Deny");
319
+ }, 30_000);
320
+
321
+ it("B8 — validate_template + link_template + validate consistency", async () => {
322
+ // Invariant: if a template validates against a schema, then linking it with
323
+ // valid slot values must produce a policy that ALSO validates. Catches lossy
324
+ // template-linking implementations.
325
+ const template = `permit (
326
+ principal == ?principal,
327
+ action == DocMgmt::Action::"read",
328
+ resource == ?resource
329
+ );`;
330
+
331
+ const validateTplRaw = await client!.callTool({
332
+ name: "cedar_validate_template",
333
+ arguments: { template, schema: SCHEMA_JSON },
334
+ });
335
+ const validateTpl = parseToolResult(validateTplRaw) as { valid: boolean };
336
+ expect(validateTpl.valid).toBe(true);
337
+
338
+ const linkRaw = await client!.callTool({
339
+ name: "cedar_link_template",
340
+ arguments: {
341
+ template,
342
+ principal: 'DocMgmt::User::"alice"',
343
+ resource: 'DocMgmt::Document::"d1"',
344
+ schema: SCHEMA_JSON,
345
+ },
346
+ });
347
+ const link = parseToolResult(linkRaw) as { linked_policy?: string; policy?: string; valid?: boolean };
348
+ const linkedPolicy = link.linked_policy ?? link.policy;
349
+ expect(linkedPolicy).toBeTruthy();
350
+
351
+ // The linked output should pass cedar_validate as a regular static policy
352
+ const validateLinkedRaw = await client!.callTool({
353
+ name: "cedar_validate",
354
+ arguments: { policies: linkedPolicy!, schema: SCHEMA_JSON },
355
+ });
356
+ const validateLinked = parseToolResult(validateLinkedRaw) as { valid: boolean };
357
+ expect(validateLinked.valid).toBe(true);
358
+ }, 30_000);
359
+ });