@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.
- package/CHANGELOG.md +21 -0
- package/README.md +36 -1
- package/dist/cedar-engine-cedarling.d.ts +81 -0
- package/dist/cedar-engine-cedarling.js +651 -0
- package/dist/cedar-engine-cedarling.js.map +1 -0
- package/dist/cedar-engine.d.ts +77 -0
- package/dist/cedar-engine.js +374 -0
- package/dist/cedar-engine.js.map +1 -0
- package/dist/gui/html.d.ts +5 -0
- package/dist/gui/html.js +930 -0
- package/dist/gui/html.js.map +1 -0
- package/dist/gui/server.d.ts +28 -0
- package/dist/gui/server.js +159 -0
- package/dist/gui/server.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +584 -0
- package/dist/index.js.map +1 -0
- package/dist/llm-proxy.d.ts +75 -0
- package/dist/llm-proxy.js +565 -0
- package/dist/llm-proxy.js.map +1 -0
- package/dist/mcp-aggregator.d.ts +29 -0
- package/dist/mcp-aggregator.js +144 -0
- package/dist/mcp-aggregator.js.map +1 -0
- package/dist/policy-source.d.ts +26 -0
- package/dist/policy-source.js +28 -0
- package/dist/policy-source.js.map +1 -0
- package/dist/types.d.ts +135 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/docs/carapace_proxy_tool_filter_flow_v3.svg +96 -0
- package/docs/ungated_ai_agent_capabilities.svg +143 -0
- package/package.json +1 -1
- package/src/cedar-engine-cedarling.ts +64 -5
- package/src/gui/server.ts +17 -0
- package/src/index.ts +2 -0
- package/src/llm-proxy.ts +21 -0
- package/src/policy-source.ts +44 -0
- package/vitest.config.ts +8 -0
|
@@ -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
|