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.
- package/.editorconfig +12 -0
- package/.github/workflows/ci.yml +31 -0
- package/.github/workflows/release.yml +42 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +241 -0
- package/CONTRIBUTING.md +83 -0
- package/LICENSE +182 -0
- package/README.md +1635 -0
- package/SECURITY.md +37 -0
- package/dist/http-server.d.ts +61 -0
- package/dist/http-server.d.ts.map +1 -0
- package/dist/http-server.js +194 -0
- package/dist/http-server.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +270 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/policy-ast.d.ts +49 -0
- package/dist/parser/policy-ast.d.ts.map +1 -0
- package/dist/parser/policy-ast.js +311 -0
- package/dist/parser/policy-ast.js.map +1 -0
- package/dist/prompts/index.d.ts +38 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +172 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/resources/ref-resolver.d.ts +23 -0
- package/dist/resources/ref-resolver.d.ts.map +1 -0
- package/dist/resources/ref-resolver.js +128 -0
- package/dist/resources/ref-resolver.js.map +1 -0
- package/dist/resources/store-manager.d.ts +64 -0
- package/dist/resources/store-manager.d.ts.map +1 -0
- package/dist/resources/store-manager.js +221 -0
- package/dist/resources/store-manager.js.map +1 -0
- package/dist/server.d.ts +18 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +539 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/advise/avp-rules.d.ts +49 -0
- package/dist/tools/advise/avp-rules.d.ts.map +1 -0
- package/dist/tools/advise/avp-rules.js +59 -0
- package/dist/tools/advise/avp-rules.js.map +1 -0
- package/dist/tools/advise/cedar-patterns.d.ts +24 -0
- package/dist/tools/advise/cedar-patterns.d.ts.map +1 -0
- package/dist/tools/advise/cedar-patterns.js +57 -0
- package/dist/tools/advise/cedar-patterns.js.map +1 -0
- package/dist/tools/advise/context-builder.d.ts +28 -0
- package/dist/tools/advise/context-builder.d.ts.map +1 -0
- package/dist/tools/advise/context-builder.js +89 -0
- package/dist/tools/advise/context-builder.js.map +1 -0
- package/dist/tools/advise/gotchas.d.ts +15 -0
- package/dist/tools/advise/gotchas.d.ts.map +1 -0
- package/dist/tools/advise/gotchas.js +83 -0
- package/dist/tools/advise/gotchas.js.map +1 -0
- package/dist/tools/advise.d.ts +96 -0
- package/dist/tools/advise.d.ts.map +1 -0
- package/dist/tools/advise.js +258 -0
- package/dist/tools/advise.js.map +1 -0
- package/dist/tools/authorize-batch.d.ts +35 -0
- package/dist/tools/authorize-batch.d.ts.map +1 -0
- package/dist/tools/authorize-batch.js +262 -0
- package/dist/tools/authorize-batch.js.map +1 -0
- package/dist/tools/authorize.d.ts +115 -0
- package/dist/tools/authorize.d.ts.map +1 -0
- package/dist/tools/authorize.js +373 -0
- package/dist/tools/authorize.js.map +1 -0
- package/dist/tools/check-change.d.ts +19 -0
- package/dist/tools/check-change.d.ts.map +1 -0
- package/dist/tools/check-change.js +91 -0
- package/dist/tools/check-change.js.map +1 -0
- package/dist/tools/diff-schema.d.ts +103 -0
- package/dist/tools/diff-schema.d.ts.map +1 -0
- package/dist/tools/diff-schema.js +379 -0
- package/dist/tools/diff-schema.js.map +1 -0
- package/dist/tools/diff-stores.d.ts +45 -0
- package/dist/tools/diff-stores.d.ts.map +1 -0
- package/dist/tools/diff-stores.js +222 -0
- package/dist/tools/diff-stores.js.map +1 -0
- package/dist/tools/explain.d.ts +80 -0
- package/dist/tools/explain.d.ts.map +1 -0
- package/dist/tools/explain.js +187 -0
- package/dist/tools/explain.js.map +1 -0
- package/dist/tools/format.d.ts +11 -0
- package/dist/tools/format.d.ts.map +1 -0
- package/dist/tools/format.js +20 -0
- package/dist/tools/format.js.map +1 -0
- package/dist/tools/generate-sample.d.ts +28 -0
- package/dist/tools/generate-sample.d.ts.map +1 -0
- package/dist/tools/generate-sample.js +568 -0
- package/dist/tools/generate-sample.js.map +1 -0
- package/dist/tools/link-template.d.ts +17 -0
- package/dist/tools/link-template.d.ts.map +1 -0
- package/dist/tools/link-template.js +78 -0
- package/dist/tools/link-template.js.map +1 -0
- package/dist/tools/list-template-links.d.ts +16 -0
- package/dist/tools/list-template-links.d.ts.map +1 -0
- package/dist/tools/list-template-links.js +22 -0
- package/dist/tools/list-template-links.js.map +1 -0
- package/dist/tools/list-templates.d.ts +16 -0
- package/dist/tools/list-templates.d.ts.map +1 -0
- package/dist/tools/list-templates.js +36 -0
- package/dist/tools/list-templates.js.map +1 -0
- package/dist/tools/translate.d.ts +11 -0
- package/dist/tools/translate.d.ts.map +1 -0
- package/dist/tools/translate.js +53 -0
- package/dist/tools/translate.js.map +1 -0
- package/dist/tools/validate-entities.d.ts +19 -0
- package/dist/tools/validate-entities.d.ts.map +1 -0
- package/dist/tools/validate-entities.js +88 -0
- package/dist/tools/validate-entities.js.map +1 -0
- package/dist/tools/validate-schema.d.ts +22 -0
- package/dist/tools/validate-schema.d.ts.map +1 -0
- package/dist/tools/validate-schema.js +89 -0
- package/dist/tools/validate-schema.js.map +1 -0
- package/dist/tools/validate-template.d.ts +18 -0
- package/dist/tools/validate-template.d.ts.map +1 -0
- package/dist/tools/validate-template.js +59 -0
- package/dist/tools/validate-template.js.map +1 -0
- package/dist/tools/validate.d.ts +90 -0
- package/dist/tools/validate.d.ts.map +1 -0
- package/dist/tools/validate.js +351 -0
- package/dist/tools/validate.js.map +1 -0
- package/dist/utils/format-detector.d.ts +49 -0
- package/dist/utils/format-detector.d.ts.map +1 -0
- package/dist/utils/format-detector.js +298 -0
- package/dist/utils/format-detector.js.map +1 -0
- package/examples/README.md +36 -0
- package/examples/abac-multi-tenant/README.md +150 -0
- package/examples/abac-multi-tenant/entities/users-and-docs.json +33 -0
- package/examples/abac-multi-tenant/policies/member-read-internal.cedar +9 -0
- package/examples/abac-multi-tenant/policies/owner-full-access.cedar +9 -0
- package/examples/abac-multi-tenant/policies/premium-share-guard.cedar +9 -0
- package/examples/abac-multi-tenant/policies/private-doc-guard.cedar +13 -0
- package/examples/abac-multi-tenant/run.ts +92 -0
- package/examples/abac-multi-tenant/schema.json +60 -0
- package/examples/api-gateway-path-routing/README.md +154 -0
- package/examples/api-gateway-path-routing/entities/users-and-roles.json +20 -0
- package/examples/api-gateway-path-routing/policies/admin-full-access.cedar +6 -0
- package/examples/api-gateway-path-routing/policies/developer-projects.cedar +14 -0
- package/examples/api-gateway-path-routing/policies/viewer-readonly.cedar +10 -0
- package/examples/api-gateway-path-routing/run.ts +108 -0
- package/examples/api-gateway-path-routing/schema.json +54 -0
- package/examples/rbac-document-management/README.md +167 -0
- package/examples/rbac-document-management/entities/users-and-docs.json +43 -0
- package/examples/rbac-document-management/policies/admin.cedar +6 -0
- package/examples/rbac-document-management/policies/editor.cedar +6 -0
- package/examples/rbac-document-management/policies/top-secret-forbid.cedar +13 -0
- package/examples/rbac-document-management/policies/viewer.cedar +6 -0
- package/examples/rbac-document-management/run.ts +87 -0
- package/examples/rbac-document-management/schema.json +57 -0
- package/package.json +50 -0
- package/src/http-server.ts +239 -0
- package/src/index.ts +294 -0
- package/src/parser/policy-ast.ts +345 -0
- package/src/prompts/README.md +3 -0
- package/src/prompts/index.ts +217 -0
- package/src/resources/ref-resolver.ts +134 -0
- package/src/resources/store-manager.ts +248 -0
- package/src/server.ts +711 -0
- package/src/tools/advise/avp-rules.ts +70 -0
- package/src/tools/advise/cedar-patterns.ts +73 -0
- package/src/tools/advise/context-builder.ts +109 -0
- package/src/tools/advise/gotchas.ts +92 -0
- package/src/tools/advise.ts +366 -0
- package/src/tools/authorize-batch.ts +345 -0
- package/src/tools/authorize.ts +464 -0
- package/src/tools/check-change.ts +119 -0
- package/src/tools/diff-schema.ts +510 -0
- package/src/tools/diff-stores.ts +298 -0
- package/src/tools/explain.ts +278 -0
- package/src/tools/format.ts +33 -0
- package/src/tools/generate-sample.ts +665 -0
- package/src/tools/link-template.ts +109 -0
- package/src/tools/list-template-links.ts +41 -0
- package/src/tools/list-templates.ts +55 -0
- package/src/tools/translate.ts +66 -0
- package/src/tools/validate-entities.ts +125 -0
- package/src/tools/validate-schema.ts +128 -0
- package/src/tools/validate-template.ts +72 -0
- package/src/tools/validate.ts +459 -0
- package/src/utils/format-detector.ts +356 -0
- package/test/fixtures/docmgmt.ts +121 -0
- package/test/fixtures/multitenant.ts +163 -0
- package/test/index.test.ts +96 -0
- package/test/integration/e2e/behavior.test.ts +359 -0
- package/test/integration/e2e/edge-cases.test.ts +365 -0
- package/test/integration/e2e/failure-modes.test.ts +266 -0
- package/test/integration/e2e/protocol.test.ts +252 -0
- package/test/integration/http-smoke.test.ts +588 -0
- package/test/integration/smoke.test.ts +475 -0
- package/test/prompts/prompts.test.ts +173 -0
- package/test/property/properties.test.ts +234 -0
- package/test/resources/ref-resolver.test.ts +186 -0
- package/test/resources/store-manager.test.ts +344 -0
- package/test/setup.test.ts +7 -0
- package/test/tools/advise/avp-rules.test.ts +76 -0
- package/test/tools/advise.test.ts +339 -0
- package/test/tools/authorize-batch.test.ts +459 -0
- package/test/tools/authorize.test.ts +682 -0
- package/test/tools/check-change.test.ts +104 -0
- package/test/tools/cross-fixture.test.ts +170 -0
- package/test/tools/diff-schema.test.ts +355 -0
- package/test/tools/diff-stores.test.ts +291 -0
- package/test/tools/explain.test.ts +221 -0
- package/test/tools/format.test.ts +33 -0
- package/test/tools/generate-sample.test.ts +480 -0
- package/test/tools/link-template.test.ts +90 -0
- package/test/tools/list-templates.test.ts +151 -0
- package/test/tools/translate.test.ts +89 -0
- package/test/tools/validate-entities.test.ts +178 -0
- package/test/tools/validate-schema.test.ts +86 -0
- package/test/tools/validate-template.test.ts +89 -0
- package/test/tools/validate.test.ts +331 -0
- package/test/utils/format-detector.test.ts +518 -0
- package/tsconfig.json +17 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { handleAdvise } from "../../src/tools/advise.js";
|
|
6
|
+
import { StoreManager } from "../../src/resources/store-manager.js";
|
|
7
|
+
|
|
8
|
+
// ─── Fixture data (Dataset 1: DocMgmt, NDA-safe) ──────────────────────────────
|
|
9
|
+
|
|
10
|
+
const SCHEMA_CEDARSCHEMA = `namespace DocMgmt {
|
|
11
|
+
entity User in [Role] = { name: String, email: String };
|
|
12
|
+
entity Role;
|
|
13
|
+
entity Document = { classification: String, business_unit: String };
|
|
14
|
+
action READ appliesTo { principal: [User], resource: [Document], context: {} };
|
|
15
|
+
action WRITE appliesTo { principal: [User], resource: [Document], context: {} };
|
|
16
|
+
}`;
|
|
17
|
+
|
|
18
|
+
const SCHEMA_JSON = JSON.stringify({
|
|
19
|
+
DocMgmt: {
|
|
20
|
+
entityTypes: {
|
|
21
|
+
User: { memberOfTypes: ["Role"], shape: { type: "Record", attributes: { name: { type: "String", required: true } } } },
|
|
22
|
+
Role: { shape: { type: "Record", attributes: {} }, memberOfTypes: [] },
|
|
23
|
+
Document: { shape: { type: "Record", attributes: { classification: { type: "String", required: true } } } },
|
|
24
|
+
},
|
|
25
|
+
actions: {
|
|
26
|
+
READ: { appliesTo: { principalTypes: ["User"], resourceTypes: ["Document"], context: { type: "Record", attributes: {} } } },
|
|
27
|
+
WRITE: { appliesTo: { principalTypes: ["User"], resourceTypes: ["Document"], context: { type: "Record", attributes: {} } } },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const ADMIN_POLICY = `permit(principal in DocMgmt::Role::"admin", action, resource);`;
|
|
33
|
+
const EDITOR_POLICY = `permit(principal in DocMgmt::Role::"editor", action == DocMgmt::Action::"READ", resource);`;
|
|
34
|
+
const REBAC_POLICY = `permit(principal is DocMgmt::User, action == DocMgmt::Action::"READ", resource is DocMgmt::Document) when { principal in resource.owners };`;
|
|
35
|
+
|
|
36
|
+
function makeStore(baseDir: string, name: string, policies: Record<string, string>, schema = SCHEMA_CEDARSCHEMA, schemaFile = "schema.cedarschema"): string {
|
|
37
|
+
const path = join(baseDir, name);
|
|
38
|
+
mkdirSync(join(path, "policies"), { recursive: true });
|
|
39
|
+
for (const [id, content] of Object.entries(policies)) {
|
|
40
|
+
writeFileSync(join(path, "policies", `${id}.cedar`), content);
|
|
41
|
+
}
|
|
42
|
+
writeFileSync(join(path, schemaFile), schema);
|
|
43
|
+
return path;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
describe("cedar_advise — context preparator (v2, no sampling)", () => {
|
|
49
|
+
let tmpDir: string;
|
|
50
|
+
let manager: StoreManager;
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
tmpDir = join(tmpdir(), `cedar-advise-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
54
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
55
|
+
manager = new StoreManager();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ─── Pivot guarantees: no sampler, deterministic, synchronous-shaped ────────
|
|
63
|
+
|
|
64
|
+
it("B1 — handleAdvise is a pure function: no sampler argument, no async LLM call", () => {
|
|
65
|
+
// The signature accepts (input, manager?) — no sampler. Calling it returns a value
|
|
66
|
+
// synchronously without awaiting any client LLM round-trip.
|
|
67
|
+
const result = handleAdvise({ intent: "test" });
|
|
68
|
+
expect(result).toBeDefined();
|
|
69
|
+
expect(result.tool).toBe("cedar_advise");
|
|
70
|
+
expect(result.bundle_version).toBe("v2");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("B2 — deterministic: two calls with identical inputs return identical bundles", () => {
|
|
74
|
+
const a = handleAdvise({ intent: "Restrict read to verified email users" });
|
|
75
|
+
const b = handleAdvise({ intent: "Restrict read to verified email users" });
|
|
76
|
+
expect(a).toEqual(b);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ─── Bundle universal payload (independent of store_ref) ────────────────────
|
|
80
|
+
|
|
81
|
+
it("B3 — bundle echoes intent verbatim", () => {
|
|
82
|
+
const intent = "Only admins should delete top-secret documents";
|
|
83
|
+
const result = handleAdvise({ intent });
|
|
84
|
+
expect(result.intent).toBe(intent);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("B4 — bundle includes Cedar patterns reference with all four patterns", () => {
|
|
88
|
+
const result = handleAdvise({ intent: "anything" });
|
|
89
|
+
const names = result.cedar_patterns_reference.patterns.map(p => p.name);
|
|
90
|
+
expect(names).toEqual(expect.arrayContaining([
|
|
91
|
+
expect.stringMatching(/Membership/),
|
|
92
|
+
expect.stringMatching(/Relationship/),
|
|
93
|
+
expect.stringMatching(/Discretionary/),
|
|
94
|
+
expect.stringMatching(/Hybrid/),
|
|
95
|
+
]));
|
|
96
|
+
expect(result.cedar_patterns_reference.summary).toContain("MEMBERSHIP");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("B5 — bundle includes AVP UpdatePolicy rules with three buckets", () => {
|
|
100
|
+
const result = handleAdvise({ intent: "anything" });
|
|
101
|
+
expect(result.avp_update_policy_rules.in_place_via_update_policy.length).toBeGreaterThan(0);
|
|
102
|
+
expect(result.avp_update_policy_rules.requires_delete_recreate.length).toBeGreaterThan(0);
|
|
103
|
+
expect(result.avp_update_policy_rules.new_via_create_policy.length).toBeGreaterThan(0);
|
|
104
|
+
expect(result.avp_update_policy_rules.summary).toContain("UpdatePolicy");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("B6 — bundle includes AVP validation error catalog", () => {
|
|
108
|
+
const result = handleAdvise({ intent: "anything" });
|
|
109
|
+
const ids = result.avp_validation_error_catalog.map(e => e.id);
|
|
110
|
+
expect(ids).toContain("UnsafeOptionalAttributeAccess");
|
|
111
|
+
expect(ids).toContain("UnrecognizedEntityType");
|
|
112
|
+
expect(ids).toContain("MissingAttribute");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("B7 — bundle includes sequencing_guidance with schema-before-policy rule", () => {
|
|
116
|
+
const result = handleAdvise({ intent: "anything" });
|
|
117
|
+
expect(result.sequencing_guidance.length).toBeGreaterThan(0);
|
|
118
|
+
const joined = result.sequencing_guidance.join(" ");
|
|
119
|
+
expect(joined).toMatch(/schema.*before.*polic/i);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("B8 — bundle includes next_steps_for_llm directing follow-up tool calls", () => {
|
|
123
|
+
const result = handleAdvise({ intent: "anything" });
|
|
124
|
+
expect(result.next_steps_for_llm).toContain("cedar_validate");
|
|
125
|
+
expect(result.next_steps_for_llm).toContain("cedar_check_policy_change");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ─── Gotcha selection from intent keywords ───────────────────────────────────
|
|
129
|
+
|
|
130
|
+
it("B9 — selects optional_attribute_guard gotcha for verified-email intent", () => {
|
|
131
|
+
const result = handleAdvise({
|
|
132
|
+
intent: "Require verified email attribute before granting read access",
|
|
133
|
+
});
|
|
134
|
+
const ids = result.applicable_gotchas.map(g => g.id);
|
|
135
|
+
expect(ids).toContain("optional_attribute_guard");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("B10 — selects forbid_overrides_permit gotcha for block/deny intent", () => {
|
|
139
|
+
const result = handleAdvise({
|
|
140
|
+
intent: "Block all access to sensitive top-secret documents except admins",
|
|
141
|
+
});
|
|
142
|
+
const ids = result.applicable_gotchas.map(g => g.id);
|
|
143
|
+
expect(ids).toContain("forbid_overrides_permit");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("B11 — selects rebac_migration_data gotcha for relationship/owners intent", () => {
|
|
147
|
+
const result = handleAdvise({
|
|
148
|
+
intent: "Migrate to per-resource owner-based access (owners attribute on Document)",
|
|
149
|
+
});
|
|
150
|
+
const ids = result.applicable_gotchas.map(g => g.id);
|
|
151
|
+
expect(ids).toContain("rebac_migration_data");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("B12 — no gotcha keywords matched: applicable_gotchas is empty but bundle still valid", () => {
|
|
155
|
+
const result = handleAdvise({ intent: "xyzzy plugh frobnitz" });
|
|
156
|
+
expect(result.applicable_gotchas).toEqual([]);
|
|
157
|
+
// Universal sections still populated
|
|
158
|
+
expect(result.cedar_patterns_reference.patterns.length).toBeGreaterThan(0);
|
|
159
|
+
expect(result.avp_update_policy_rules.in_place_via_update_policy.length).toBeGreaterThan(0);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// ─── Store context handling ─────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
it("B13 — store_ref omitted: store_status='not_provided', no schema, empty inventory", () => {
|
|
165
|
+
const result = handleAdvise({ intent: "anything" });
|
|
166
|
+
expect(result.store_status).toBe("not_provided");
|
|
167
|
+
expect(result.store_name).toBeUndefined();
|
|
168
|
+
expect(result.schema_summary).toBeUndefined();
|
|
169
|
+
expect(result.policy_inventory).toEqual([]);
|
|
170
|
+
expect(result.patterns_detected_in_store).toEqual([]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("B14 — store_ref unknown: store_status='not_found', store_name preserved", () => {
|
|
174
|
+
const result = handleAdvise({ intent: "anything", store_ref: "nonexistent" }, manager);
|
|
175
|
+
expect(result.store_status).toBe("not_found");
|
|
176
|
+
expect(result.store_name).toBe("nonexistent");
|
|
177
|
+
expect(result.schema_summary).toBeUndefined();
|
|
178
|
+
expect(result.policy_inventory).toEqual([]);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("B15 — store_ref loaded: bundle includes schema_summary, inventory, pattern counts", () => {
|
|
182
|
+
const storePath = makeStore(tmpDir, "mystore", { admin: ADMIN_POLICY, editor: EDITOR_POLICY });
|
|
183
|
+
manager.loadFromRoots([{ uri: `file://${storePath}`, name: "mystore" }]);
|
|
184
|
+
|
|
185
|
+
const result = handleAdvise({ intent: "Add ABAC condition", store_ref: "mystore" }, manager);
|
|
186
|
+
|
|
187
|
+
expect(result.store_status).toBe("loaded");
|
|
188
|
+
expect(result.store_name).toBe("mystore");
|
|
189
|
+
expect(result.schema_summary?.valid).toBe(true);
|
|
190
|
+
expect(result.schema_summary?.format).toBe("cedarschema");
|
|
191
|
+
expect(result.schema_summary?.namespaces).toContain("DocMgmt");
|
|
192
|
+
expect(result.policy_inventory).toHaveLength(2);
|
|
193
|
+
expect(result.policy_inventory.map(p => p.policy_id).sort()).toEqual(["admin", "editor"]);
|
|
194
|
+
// Both admin and editor use principal-in-Role → Membership pattern
|
|
195
|
+
const membershipCount = result.patterns_detected_in_store.find(p => p.pattern === "membership")?.count;
|
|
196
|
+
expect(membershipCount).toBe(2);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("B16 — store_ref accepts cedar:// URI form", () => {
|
|
200
|
+
const storePath = makeStore(tmpDir, "production", { admin: ADMIN_POLICY });
|
|
201
|
+
manager.loadFromRoots([{ uri: `file://${storePath}`, name: "production" }]);
|
|
202
|
+
|
|
203
|
+
const result = handleAdvise(
|
|
204
|
+
{ intent: "Update policy", store_ref: "cedar://policies/production" },
|
|
205
|
+
manager
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
expect(result.store_status).toBe("loaded");
|
|
209
|
+
expect(result.store_name).toBe("production");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("B17 — policy_inventory entries include full Cedar text so the LLM can quote exact scope", () => {
|
|
213
|
+
const storePath = makeStore(tmpDir, "withtext", { admin: ADMIN_POLICY, editor: EDITOR_POLICY });
|
|
214
|
+
manager.loadFromRoots([{ uri: `file://${storePath}`, name: "withtext" }]);
|
|
215
|
+
|
|
216
|
+
const result = handleAdvise({ intent: "Modify editor permissions", store_ref: "withtext" }, manager);
|
|
217
|
+
|
|
218
|
+
const editor = result.policy_inventory.find(p => p.policy_id === "editor");
|
|
219
|
+
expect(editor).toBeDefined();
|
|
220
|
+
expect(editor!.policy_text).toContain('Role::"editor"');
|
|
221
|
+
expect(editor!.policy_text).toContain('Action::"READ"');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("B18 — ReBAC policy classified as relationship pattern in patterns_detected_in_store", () => {
|
|
225
|
+
const storePath = makeStore(tmpDir, "rebac", { owner: REBAC_POLICY });
|
|
226
|
+
manager.loadFromRoots([{ uri: `file://${storePath}`, name: "rebac" }]);
|
|
227
|
+
|
|
228
|
+
const result = handleAdvise({ intent: "Audit ReBAC policies", store_ref: "rebac" }, manager);
|
|
229
|
+
|
|
230
|
+
const relationshipCount = result.patterns_detected_in_store.find(p => p.pattern === "relationship")?.count;
|
|
231
|
+
expect(relationshipCount).toBe(1);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("B19 — JSON-format schema parsed and summarized correctly", () => {
|
|
235
|
+
const storePath = makeStore(tmpDir, "jsonstore", { admin: ADMIN_POLICY }, SCHEMA_JSON, "schema.json");
|
|
236
|
+
manager.loadFromRoots([{ uri: `file://${storePath}`, name: "jsonstore" }]);
|
|
237
|
+
|
|
238
|
+
const result = handleAdvise({ intent: "anything", store_ref: "jsonstore" }, manager);
|
|
239
|
+
|
|
240
|
+
expect(result.schema_summary?.valid).toBe(true);
|
|
241
|
+
expect(result.schema_summary?.format).toBe("json");
|
|
242
|
+
expect(result.schema_summary?.namespaces).toEqual(["DocMgmt"]);
|
|
243
|
+
expect(result.schema_summary?.entity_type_count).toBe(3);
|
|
244
|
+
expect(result.schema_summary?.action_count).toBe(2);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// ─── MCP tool description contract (for the bypass-prevention case) ─────────
|
|
248
|
+
|
|
249
|
+
it("B20 — bundle is self-describing: tool name + version + intent recorded for downstream auditing", () => {
|
|
250
|
+
const result = handleAdvise({ intent: "Add a deletion permission" });
|
|
251
|
+
expect(result.tool).toBe("cedar_advise");
|
|
252
|
+
expect(result.bundle_version).toBe("v2");
|
|
253
|
+
expect(result.intent).toBe("Add a deletion permission");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// ─── 11b: store auto-resolve + discoverability ─────────────────────────────
|
|
257
|
+
|
|
258
|
+
it("B21 — no store_ref + zero stores loaded: store_status='not_provided', no available_stores", () => {
|
|
259
|
+
const result = handleAdvise({ intent: "anything" }, manager);
|
|
260
|
+
expect(result.store_status).toBe("not_provided");
|
|
261
|
+
expect(result.store_name).toBeUndefined();
|
|
262
|
+
expect(result.available_stores).toBeUndefined();
|
|
263
|
+
expect(result.auto_discovered).toBeUndefined();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("B22 — no store_ref + exactly one store loaded: auto-resolves to that store, store_status='loaded', auto_discovered.store_from='single_loaded_store'", () => {
|
|
267
|
+
const storePath = makeStore(tmpDir, "onlystore", { admin: ADMIN_POLICY });
|
|
268
|
+
manager.loadFromRoots([{ uri: `file://${storePath}`, name: "onlystore" }]);
|
|
269
|
+
|
|
270
|
+
const result = handleAdvise({ intent: "Plan a change" }, manager);
|
|
271
|
+
|
|
272
|
+
expect(result.store_status).toBe("loaded");
|
|
273
|
+
expect(result.store_name).toBe("onlystore");
|
|
274
|
+
expect(result.auto_discovered).toEqual({ store_from: "single_loaded_store" });
|
|
275
|
+
expect(result.schema_summary?.valid).toBe(true);
|
|
276
|
+
expect(result.policy_inventory).toHaveLength(1);
|
|
277
|
+
// available_stores is irrelevant on a clean resolution; should be omitted to keep the payload small
|
|
278
|
+
expect(result.available_stores).toBeUndefined();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("B23 — no store_ref + multiple stores loaded: store_status='ambiguous', available_stores lists candidates, no schema_summary", () => {
|
|
282
|
+
const blue = makeStore(tmpDir, "blue", { admin: ADMIN_POLICY });
|
|
283
|
+
const green = makeStore(tmpDir, "green", { admin: ADMIN_POLICY });
|
|
284
|
+
manager.loadFromRoots([
|
|
285
|
+
{ uri: `file://${blue}`, name: "blue" },
|
|
286
|
+
{ uri: `file://${green}`, name: "green" },
|
|
287
|
+
]);
|
|
288
|
+
|
|
289
|
+
const result = handleAdvise({ intent: "Plan a change" }, manager);
|
|
290
|
+
|
|
291
|
+
expect(result.store_status).toBe("ambiguous");
|
|
292
|
+
expect(result.store_name).toBeUndefined();
|
|
293
|
+
expect(result.available_stores).toEqual(expect.arrayContaining(["blue", "green"]));
|
|
294
|
+
expect(result.available_stores).toHaveLength(2);
|
|
295
|
+
expect(result.schema_summary).toBeUndefined();
|
|
296
|
+
expect(result.policy_inventory).toEqual([]);
|
|
297
|
+
expect(result.auto_discovered).toBeUndefined();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("B24 — store_ref provided but not found + other stores loaded: store_status='not_found', available_stores hints recovery", () => {
|
|
301
|
+
const storePath = makeStore(tmpDir, "production", { admin: ADMIN_POLICY });
|
|
302
|
+
manager.loadFromRoots([{ uri: `file://${storePath}`, name: "production" }]);
|
|
303
|
+
|
|
304
|
+
const result = handleAdvise({ intent: "anything", store_ref: "stagign" }, manager);
|
|
305
|
+
|
|
306
|
+
expect(result.store_status).toBe("not_found");
|
|
307
|
+
expect(result.store_name).toBe("stagign");
|
|
308
|
+
expect(result.available_stores).toEqual(["production"]);
|
|
309
|
+
expect(result.schema_summary).toBeUndefined();
|
|
310
|
+
expect(result.policy_inventory).toEqual([]);
|
|
311
|
+
expect(result.auto_discovered).toBeUndefined();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("B25 — 11d audit finding: schemaless single-store auto-resolve degrades to 'not_provided', not self-referential 'not_found'", () => {
|
|
315
|
+
// A workspace with `policies/` but no schema file triggers cwd-fallback in
|
|
316
|
+
// index.ts (looksLikeCedarWorkspace passes on policies/ alone) and lands as
|
|
317
|
+
// a single loaded store. buildStoreContext returns null because readSchema
|
|
318
|
+
// throws. The pre-fix branch surfaced this as `not_found` with
|
|
319
|
+
// store_name === available_stores[0], creating a self-referential response
|
|
320
|
+
// that the next_steps_for_llm "re-invoke with corrected name" advice would
|
|
321
|
+
// loop on. After the fix the response degrades to `not_provided`: the LLM
|
|
322
|
+
// gets the universal Cedar / AVP context and treats this as a store-less
|
|
323
|
+
// call, no loop.
|
|
324
|
+
const path = join(tmpDir, "schemaless");
|
|
325
|
+
mkdirSync(join(path, "policies"), { recursive: true });
|
|
326
|
+
writeFileSync(join(path, "policies", "admin.cedar"), ADMIN_POLICY);
|
|
327
|
+
// Deliberately NO schema.cedarschema / schema.json
|
|
328
|
+
manager.loadFromRoots([{ uri: `file://${path}`, name: "schemaless" }]);
|
|
329
|
+
|
|
330
|
+
const result = handleAdvise({ intent: "plan a change" }, manager);
|
|
331
|
+
|
|
332
|
+
expect(result.store_status).toBe("not_provided");
|
|
333
|
+
expect(result.store_name).toBeUndefined();
|
|
334
|
+
expect(result.available_stores).toBeUndefined();
|
|
335
|
+
expect(result.auto_discovered).toBeUndefined();
|
|
336
|
+
// Universal context still present.
|
|
337
|
+
expect(result.cedar_patterns_reference.patterns.length).toBeGreaterThan(0);
|
|
338
|
+
});
|
|
339
|
+
});
|