@clawdreyhepburn/carapace 0.3.2 → 0.4.1

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.
@@ -0,0 +1,651 @@
1
+ /**
2
+ * Cedarling-powered Cedar engine for MCP tool authorization.
3
+ *
4
+ * Uses Gluu's Cedarling WASM module for proper Cedar evaluation,
5
+ * JWT validation, and the Policy Store format. Falls back to the
6
+ * homebrew engine if WASM loading fails.
7
+ */
8
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, unlinkSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { homedir } from "node:os";
11
+ import { fileURLToPath } from "node:url";
12
+ import { dirname } from "node:path";
13
+ export class CedarlingEngine {
14
+ policyDir;
15
+ defaultPolicy;
16
+ shouldVerify;
17
+ logger;
18
+ namespace;
19
+ agentEntityType;
20
+ // Cedarling state
21
+ cedarling = null;
22
+ wasmModule = null;
23
+ // Policy/schema storage (mirrors disk, used to rebuild policy store)
24
+ policies = new Map();
25
+ schemaJson = null;
26
+ schemaRaw = "";
27
+ constructor(opts) {
28
+ this.policyDir = opts.policyDir.replace("~", homedir());
29
+ this.defaultPolicy = opts.defaultPolicy;
30
+ this.shouldVerify = opts.verify;
31
+ this.logger = opts.logger;
32
+ this.namespace = opts.namespace ?? "Jans";
33
+ this.agentEntityType = opts.agentEntityType ?? "Workload";
34
+ }
35
+ async init() {
36
+ mkdirSync(this.policyDir, { recursive: true });
37
+ // Try to load Cedarling WASM
38
+ try {
39
+ await this.loadWasm();
40
+ }
41
+ catch (err) {
42
+ this.logger.warn(`Cedarling WASM not available, falling back to basic engine: ${err.message}`);
43
+ }
44
+ // Load existing policies from disk
45
+ this.loadPoliciesFromDisk();
46
+ // Generate default schema if none exists
47
+ const schemaPath = join(this.policyDir, "schema.json");
48
+ if (!existsSync(schemaPath)) {
49
+ this.writeDefaultSchema();
50
+ }
51
+ this.schemaRaw = readFileSync(schemaPath, "utf-8");
52
+ try {
53
+ this.schemaJson = JSON.parse(this.schemaRaw);
54
+ }
55
+ catch {
56
+ this.logger.warn("Failed to parse schema.json");
57
+ }
58
+ // Create Cedarling instance
59
+ await this.rebuildCedarling();
60
+ this.logger.info(`Cedarling engine initialized: ${this.policies.size} policies, ` +
61
+ `WASM ${this.cedarling ? "active" : "unavailable"}`);
62
+ }
63
+ /**
64
+ * Authorize a request using Cedarling WASM.
65
+ */
66
+ async authorize(request) {
67
+ if (!this.cedarling) {
68
+ // Fallback: basic string matching (same as homebrew engine)
69
+ return this.authorizeBasic(request);
70
+ }
71
+ try {
72
+ // Build the principal ID from the request
73
+ const principalId = request.principal
74
+ .replace(/.*::"/g, "")
75
+ .replace(/"$/, "");
76
+ const resourceId = request.resource
77
+ .replace(/.*::"/g, "")
78
+ .replace(/"$/, "");
79
+ const actionName = request.action
80
+ .replace(/.*::"/g, "")
81
+ .replace(/"$/, "");
82
+ // Determine resource entity type from the request string
83
+ // Supports Tool::"x", Shell::"x", API::"x", etc.
84
+ let resourceEntityType = "Tool";
85
+ const typeMatch = request.resource.match(/^(?:\w+::)?(\w+)::/);
86
+ if (typeMatch)
87
+ resourceEntityType = typeMatch[1];
88
+ const cedarContext = { ...(request.context ?? {}) };
89
+ const effectivePrincipalId = principalId;
90
+ const result = await this.cedarling.authorize_unsigned({
91
+ principals: [
92
+ {
93
+ cedar_entity_mapping: {
94
+ entity_type: `${this.namespace}::${this.agentEntityType}`,
95
+ id: effectivePrincipalId,
96
+ },
97
+ name: effectivePrincipalId,
98
+ },
99
+ ],
100
+ action: `${this.namespace}::Action::"${actionName}"`,
101
+ resource: {
102
+ cedar_entity_mapping: {
103
+ entity_type: `${this.namespace}::${resourceEntityType}`,
104
+ id: resourceId,
105
+ },
106
+ ...(request.context ?? {}),
107
+ },
108
+ context: cedarContext,
109
+ });
110
+ const decision = result.decision ? "allow" : "deny";
111
+ const resultJson = JSON.parse(result.json_string());
112
+ // Extract reasons from all principals
113
+ const reasons = [];
114
+ if (resultJson.principals) {
115
+ for (const [princName, princResult] of Object.entries(resultJson.principals)) {
116
+ const diag = princResult.diagnostics;
117
+ if (diag?.reason) {
118
+ for (const r of diag.reason) {
119
+ reasons.push(`${princResult.decision ? "permit" : "deny"}: ${r}`);
120
+ }
121
+ }
122
+ }
123
+ }
124
+ return { decision: decision, reasons };
125
+ }
126
+ catch (err) {
127
+ this.logger.error(`Cedarling authorize error: ${err.message}`);
128
+ return { decision: "deny", reasons: [`cedarling error: ${err.message}`] };
129
+ }
130
+ }
131
+ /**
132
+ * Enable a resource by adding a permit policy and rebuilding Cedarling.
133
+ * resourceType: "Tool" | "Shell" | "API"
134
+ * action: the Cedar action name (e.g., "call_tool", "exec_command", "call_api")
135
+ */
136
+ enableResource(qualifiedName, resourceType = "Tool", action = "call_tool") {
137
+ const slug = qualifiedName.replace(/[^a-zA-Z0-9_-]/g, "-");
138
+ const policyId = `${resourceType.toLowerCase()}-enable-${slug}`;
139
+ const raw = `permit(\n principal is ${this.namespace}::${this.agentEntityType},\n action == ${this.namespace}::Action::"${action}",\n resource == ${this.namespace}::${resourceType}::"${qualifiedName}"\n);`;
140
+ const disableId = `${resourceType.toLowerCase()}-disable-${slug}`;
141
+ this.removePolicyFile(disableId);
142
+ this.writePolicyFile(policyId, raw);
143
+ this.policies.set(policyId, { effect: "permit", raw });
144
+ this.rebuildCedarling().catch(() => { });
145
+ this.logger.info(`Enabled ${resourceType}: ${qualifiedName}`);
146
+ }
147
+ /**
148
+ * Disable a resource by adding a forbid policy and rebuilding Cedarling.
149
+ */
150
+ disableResource(qualifiedName, resourceType = "Tool", action = "call_tool") {
151
+ const slug = qualifiedName.replace(/[^a-zA-Z0-9_-]/g, "-");
152
+ const policyId = `${resourceType.toLowerCase()}-disable-${slug}`;
153
+ const raw = `forbid(\n principal,\n action == ${this.namespace}::Action::"${action}",\n resource == ${this.namespace}::${resourceType}::"${qualifiedName}"\n);`;
154
+ const enableId = `${resourceType.toLowerCase()}-enable-${slug}`;
155
+ this.removePolicyFile(enableId);
156
+ this.writePolicyFile(policyId, raw);
157
+ this.policies.set(policyId, { effect: "forbid", raw });
158
+ this.rebuildCedarling().catch(() => { });
159
+ this.logger.info(`Disabled ${resourceType}: ${qualifiedName}`);
160
+ }
161
+ /** Backwards-compatible aliases */
162
+ enableTool(qualifiedName) {
163
+ this.enableResource(qualifiedName, "Tool", "call_tool");
164
+ }
165
+ disableTool(qualifiedName) {
166
+ this.disableResource(qualifiedName, "Tool", "call_tool");
167
+ }
168
+ /**
169
+ * Check if a tool is enabled (synchronous check against local policy state).
170
+ * Checks both specific tool policies AND blanket policies (those without a specific tool name).
171
+ */
172
+ isToolEnabled(qualifiedName) {
173
+ let hasPermit = false;
174
+ let hasForbid = false;
175
+ for (const [, policy] of this.policies) {
176
+ // Check if policy specifically references this tool
177
+ const refersToTool = policy.raw.includes(`"${qualifiedName}"`);
178
+ // Check if policy is a blanket policy (no specific Tool:: reference)
179
+ const isBlanket = !policy.raw.includes('Tool::"');
180
+ if (refersToTool || isBlanket) {
181
+ if (policy.effect === "permit")
182
+ hasPermit = true;
183
+ if (policy.effect === "forbid")
184
+ hasForbid = true;
185
+ }
186
+ }
187
+ return hasPermit && !hasForbid;
188
+ }
189
+ savePolicy(id, raw) {
190
+ const effect = raw.trimStart().startsWith("forbid") ? "forbid" : "permit";
191
+ this.writePolicyFile(id, raw);
192
+ this.policies.set(id, { effect, raw });
193
+ this.rebuildCedarling().catch(() => { });
194
+ this.logger.info(`Saved policy: ${id}`);
195
+ }
196
+ deletePolicy(id) {
197
+ if (!this.policies.has(id))
198
+ return false;
199
+ this.removePolicyFile(id);
200
+ this.policies.delete(id);
201
+ this.rebuildCedarling().catch(() => { });
202
+ this.logger.info(`Deleted policy: ${id}`);
203
+ return true;
204
+ }
205
+ getDefaultPolicy() {
206
+ return this.defaultPolicy;
207
+ }
208
+ getPolicies() {
209
+ return [...this.policies.entries()].map(([id, p]) => ({ id, ...p }));
210
+ }
211
+ getSchema() {
212
+ return {
213
+ ...this.parseSchemaForGui(this.schemaRaw),
214
+ raw: this.schemaRaw,
215
+ };
216
+ }
217
+ saveSchema(raw) {
218
+ const schemaPath = join(this.policyDir, "schema.json");
219
+ writeFileSync(schemaPath, raw, "utf-8");
220
+ this.schemaRaw = raw;
221
+ try {
222
+ this.schemaJson = JSON.parse(raw);
223
+ }
224
+ catch { }
225
+ this.rebuildCedarling().catch(() => { });
226
+ this.logger.info("Schema updated");
227
+ }
228
+ async verify() {
229
+ const start = Date.now();
230
+ if (!this.cedarling) {
231
+ return {
232
+ ok: true,
233
+ issues: ["Cedarling WASM not loaded — cannot verify"],
234
+ durationMs: Date.now() - start,
235
+ };
236
+ }
237
+ // Verification: try dummy authorize requests for each resource type.
238
+ // If the policy store loaded, schemas and policies are valid.
239
+ try {
240
+ for (const [action, resType] of [["call_tool", "Tool"], ["exec_command", "Shell"], ["call_api", "API"]]) {
241
+ await this.cedarling.authorize_unsigned({
242
+ principals: [
243
+ {
244
+ cedar_entity_mapping: {
245
+ entity_type: `${this.namespace}::${this.agentEntityType}`,
246
+ id: "__verify_probe__",
247
+ },
248
+ },
249
+ ],
250
+ action: `${this.namespace}::Action::"${action}"`,
251
+ resource: {
252
+ cedar_entity_mapping: {
253
+ entity_type: `${this.namespace}::${resType}`,
254
+ id: "__verify_probe__",
255
+ },
256
+ },
257
+ context: {},
258
+ });
259
+ }
260
+ return { ok: true, issues: [], durationMs: Date.now() - start };
261
+ }
262
+ catch (err) {
263
+ return {
264
+ ok: false,
265
+ issues: [err.message],
266
+ durationMs: Date.now() - start,
267
+ };
268
+ }
269
+ }
270
+ // ── Private: WASM loading ──
271
+ async loadWasm() {
272
+ const mod = await import("@janssenproject/cedarling_wasm");
273
+ this.wasmModule = mod;
274
+ // Find the WASM binary
275
+ const modPath = fileURLToPath(import.meta.resolve("@janssenproject/cedarling_wasm"));
276
+ const wasmPath = join(dirname(modPath), "cedarling_wasm_bg.wasm");
277
+ const wasmBytes = readFileSync(wasmPath);
278
+ mod.initSync({ module: wasmBytes });
279
+ this.logger.info("Cedarling WASM loaded successfully");
280
+ }
281
+ /**
282
+ * Rebuild the Cedarling instance from current policies and schema.
283
+ * Called after any policy or schema change.
284
+ */
285
+ async rebuildCedarling() {
286
+ if (!this.wasmModule)
287
+ return;
288
+ try {
289
+ const policyStore = this.buildPolicyStore();
290
+ const config = {
291
+ CEDARLING_APPLICATION_NAME: "Carapace",
292
+ CEDARLING_POLICY_STORE_LOCAL: JSON.stringify(policyStore),
293
+ CEDARLING_LOG_TYPE: "off",
294
+ CEDARLING_USER_AUTHZ: "disabled",
295
+ CEDARLING_WORKLOAD_AUTHZ: "enabled",
296
+ CEDARLING_JWT_SIG_VALIDATION: "disabled",
297
+ CEDARLING_JWT_SIGNATURE_ALGORITHMS_SUPPORTED: ["ES256"],
298
+ CEDARLING_ID_TOKEN_TRUST_MODE: "strict",
299
+ CEDARLING_MAPPING_WORKLOAD: `${this.namespace}::${this.agentEntityType}`,
300
+ // Check if the workload principal got ALLOW
301
+ CEDARLING_PRINCIPAL_BOOLEAN_OPERATION: {
302
+ or: [
303
+ { "===": [{ var: `${this.namespace}::${this.agentEntityType}` }, "ALLOW"] },
304
+ ],
305
+ },
306
+ };
307
+ this.cedarling = await this.wasmModule.init(config);
308
+ this.logger.debug?.("Cedarling instance rebuilt");
309
+ }
310
+ catch (err) {
311
+ this.logger.error(`Failed to rebuild Cedarling: ${err.message}`);
312
+ // Don't null out cedarling — keep the old instance if it exists
313
+ }
314
+ }
315
+ /**
316
+ * Build a Cedarling Policy Store JSON from current policies and schema.
317
+ */
318
+ buildPolicyStore() {
319
+ const policies = {};
320
+ for (const [id, policy] of this.policies) {
321
+ policies[id] = {
322
+ description: id,
323
+ creation_date: new Date().toISOString(),
324
+ policy_content: Buffer.from(policy.raw).toString("base64"),
325
+ };
326
+ }
327
+ // If no policies and default is allow-all, add a default permit
328
+ if (Object.keys(policies).length === 0 && this.defaultPolicy === "allow-all") {
329
+ const raw = `permit(\n principal is ${this.namespace}::${this.agentEntityType},\n action,\n resource\n);`;
330
+ policies["default-allow"] = {
331
+ description: "Default allow-all policy",
332
+ creation_date: new Date().toISOString(),
333
+ policy_content: Buffer.from(raw).toString("base64"),
334
+ };
335
+ }
336
+ // If no policies at all, add a dummy deny to keep Cedarling happy
337
+ // (empty policy sets can cause issues)
338
+ if (Object.keys(policies).length === 0) {
339
+ const raw = `forbid(\n principal,\n action,\n resource\n) when { false };`;
340
+ policies["__default_deny__"] = {
341
+ description: "Default deny placeholder",
342
+ creation_date: new Date().toISOString(),
343
+ policy_content: Buffer.from(raw).toString("base64"),
344
+ };
345
+ }
346
+ const schemaB64 = Buffer.from(this.schemaRaw || JSON.stringify(this.buildDefaultSchemaJson())).toString("base64");
347
+ return {
348
+ cedar_version: "v4.0.0",
349
+ policy_stores: {
350
+ mcp: {
351
+ name: "Carapace",
352
+ description: "Auto-generated policy store for MCP tool authorization",
353
+ policies,
354
+ schema: schemaB64,
355
+ trusted_issuers: {},
356
+ },
357
+ },
358
+ };
359
+ }
360
+ // ── Private: disk I/O ──
361
+ loadPoliciesFromDisk() {
362
+ this.policies.clear();
363
+ if (!existsSync(this.policyDir))
364
+ return;
365
+ for (const file of readdirSync(this.policyDir)) {
366
+ if (!file.endsWith(".cedar"))
367
+ continue;
368
+ const path = join(this.policyDir, file);
369
+ const raw = readFileSync(path, "utf-8");
370
+ const id = file.replace(".cedar", "");
371
+ const effect = raw.trimStart().startsWith("forbid") ? "forbid" : "permit";
372
+ this.policies.set(id, { effect, raw });
373
+ }
374
+ }
375
+ writePolicyFile(id, raw) {
376
+ writeFileSync(join(this.policyDir, `${id}.cedar`), raw, "utf-8");
377
+ }
378
+ removePolicyFile(id) {
379
+ const path = join(this.policyDir, `${id}.cedar`);
380
+ if (existsSync(path))
381
+ unlinkSync(path);
382
+ this.policies.delete(id);
383
+ }
384
+ writeDefaultSchema() {
385
+ const schema = this.buildDefaultSchemaJson();
386
+ const schemaPath = join(this.policyDir, "schema.json");
387
+ writeFileSync(schemaPath, JSON.stringify(schema, null, 2), "utf-8");
388
+ this.logger.info("Created default Cedar JSON schema");
389
+ }
390
+ buildDefaultSchemaJson() {
391
+ return {
392
+ [this.namespace]: {
393
+ entityTypes: {
394
+ [this.agentEntityType]: {
395
+ shape: {
396
+ type: "Record",
397
+ attributes: {
398
+ name: {
399
+ type: "EntityOrCommon",
400
+ name: "String",
401
+ required: false,
402
+ },
403
+ },
404
+ },
405
+ },
406
+ Agent: {
407
+ shape: {
408
+ type: "Record",
409
+ attributes: {
410
+ role: {
411
+ type: "EntityOrCommon",
412
+ name: "String",
413
+ required: false,
414
+ },
415
+ parentChain: {
416
+ type: "Set",
417
+ element: { type: "EntityOrCommon", name: "String" },
418
+ required: false,
419
+ },
420
+ issuer: {
421
+ type: "EntityOrCommon",
422
+ name: "String",
423
+ required: false,
424
+ },
425
+ depth: {
426
+ type: "EntityOrCommon",
427
+ name: "Long",
428
+ required: false,
429
+ },
430
+ attestation_proven: {
431
+ type: "EntityOrCommon",
432
+ name: "Boolean",
433
+ required: false,
434
+ },
435
+ },
436
+ },
437
+ },
438
+ Tool: {
439
+ shape: {
440
+ type: "Record",
441
+ attributes: {
442
+ server: {
443
+ type: "EntityOrCommon",
444
+ name: "String",
445
+ required: false,
446
+ },
447
+ name: {
448
+ type: "EntityOrCommon",
449
+ name: "String",
450
+ required: false,
451
+ },
452
+ project: {
453
+ type: "EntityOrCommon",
454
+ name: "String",
455
+ required: false,
456
+ },
457
+ team: {
458
+ type: "EntityOrCommon",
459
+ name: "String",
460
+ required: false,
461
+ },
462
+ domain: {
463
+ type: "EntityOrCommon",
464
+ name: "String",
465
+ required: false,
466
+ },
467
+ },
468
+ },
469
+ },
470
+ Shell: {
471
+ shape: {
472
+ type: "Record",
473
+ attributes: {
474
+ command: {
475
+ type: "EntityOrCommon",
476
+ name: "String",
477
+ required: false,
478
+ },
479
+ workdir: {
480
+ type: "EntityOrCommon",
481
+ name: "String",
482
+ required: false,
483
+ },
484
+ },
485
+ },
486
+ },
487
+ API: {
488
+ shape: {
489
+ type: "Record",
490
+ attributes: {
491
+ url: {
492
+ type: "EntityOrCommon",
493
+ name: "String",
494
+ required: false,
495
+ },
496
+ method: {
497
+ type: "EntityOrCommon",
498
+ name: "String",
499
+ required: false,
500
+ },
501
+ domain: {
502
+ type: "EntityOrCommon",
503
+ name: "String",
504
+ required: false,
505
+ },
506
+ },
507
+ },
508
+ },
509
+ },
510
+ actions: {
511
+ call_tool: {
512
+ appliesTo: {
513
+ principalTypes: [this.agentEntityType, "Agent"],
514
+ resourceTypes: ["Tool"],
515
+ context: {
516
+ type: "Record",
517
+ attributes: {
518
+ agent_role: { type: "EntityOrCommon", name: "String", required: false },
519
+ agent_issuer: { type: "EntityOrCommon", name: "String", required: false },
520
+ agent_depth: { type: "EntityOrCommon", name: "Long", required: false },
521
+ agent_attestation_proven: { type: "EntityOrCommon", name: "Boolean", required: false },
522
+ },
523
+ },
524
+ },
525
+ },
526
+ list_tools: {
527
+ appliesTo: {
528
+ principalTypes: [this.agentEntityType],
529
+ resourceTypes: ["Tool"],
530
+ context: { type: "Record", attributes: {} },
531
+ },
532
+ },
533
+ exec_command: {
534
+ appliesTo: {
535
+ principalTypes: [this.agentEntityType],
536
+ resourceTypes: ["Shell"],
537
+ context: {
538
+ type: "Record",
539
+ attributes: {
540
+ args: {
541
+ type: "EntityOrCommon",
542
+ name: "String",
543
+ required: false,
544
+ },
545
+ workdir: {
546
+ type: "EntityOrCommon",
547
+ name: "String",
548
+ required: false,
549
+ },
550
+ },
551
+ },
552
+ },
553
+ },
554
+ call_api: {
555
+ appliesTo: {
556
+ principalTypes: [this.agentEntityType],
557
+ resourceTypes: ["API"],
558
+ context: {
559
+ type: "Record",
560
+ attributes: {
561
+ url: {
562
+ type: "EntityOrCommon",
563
+ name: "String",
564
+ required: false,
565
+ },
566
+ method: {
567
+ type: "EntityOrCommon",
568
+ name: "String",
569
+ required: false,
570
+ },
571
+ body: {
572
+ type: "EntityOrCommon",
573
+ name: "String",
574
+ required: false,
575
+ },
576
+ },
577
+ },
578
+ },
579
+ },
580
+ },
581
+ },
582
+ };
583
+ }
584
+ // ── Private: basic fallback ──
585
+ authorizeBasic(request) {
586
+ let hasPermit = false;
587
+ let hasForbid = false;
588
+ const reasons = [];
589
+ for (const [id, policy] of this.policies) {
590
+ // Simple: check if resource appears in the policy
591
+ const resourceId = request.resource.replace(/.*::"/g, "").replace(/"$/, "");
592
+ if (!policy.raw.includes(`"${resourceId}"`))
593
+ continue;
594
+ if (policy.effect === "forbid") {
595
+ hasForbid = true;
596
+ reasons.push(`forbid: ${id}`);
597
+ }
598
+ else {
599
+ hasPermit = true;
600
+ reasons.push(`permit: ${id}`);
601
+ }
602
+ }
603
+ if (hasForbid)
604
+ return { decision: "deny", reasons };
605
+ if (hasPermit)
606
+ return { decision: "allow", reasons };
607
+ return { decision: "deny", reasons: ["no matching permit policy"] };
608
+ }
609
+ // ── Private: schema parsing for GUI ──
610
+ parseSchemaForGui(raw) {
611
+ const entities = [];
612
+ const actions = [];
613
+ try {
614
+ const schema = typeof raw === "string" ? JSON.parse(raw) : raw;
615
+ const ns = schema[this.namespace];
616
+ if (!ns)
617
+ return { entities, actions };
618
+ // Entity types
619
+ if (ns.entityTypes) {
620
+ for (const [name, def] of Object.entries(ns.entityTypes)) {
621
+ const attrs = [];
622
+ if (def.shape?.attributes) {
623
+ for (const [aName, aDef] of Object.entries(def.shape.attributes)) {
624
+ attrs.push({
625
+ name: aName,
626
+ type: aDef.name || aDef.type || "unknown",
627
+ optional: aDef.required === false,
628
+ });
629
+ }
630
+ }
631
+ entities.push({ name, parents: [], attributes: attrs });
632
+ }
633
+ }
634
+ // Actions
635
+ if (ns.actions) {
636
+ for (const [name, def] of Object.entries(ns.actions)) {
637
+ actions.push({
638
+ name,
639
+ principalTypes: def.appliesTo?.principalTypes ?? [],
640
+ resourceTypes: def.appliesTo?.resourceTypes ?? [],
641
+ });
642
+ }
643
+ }
644
+ }
645
+ catch (err) {
646
+ this.logger.debug?.(`Schema parse error: ${err}`);
647
+ }
648
+ return { entities, actions };
649
+ }
650
+ }
651
+ //# sourceMappingURL=cedar-engine-cedarling.js.map