@clawdreyhepburn/carapace 1.0.4 → 1.0.6

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/dist/index.js CHANGED
@@ -1,16 +1,46 @@
1
1
  /**
2
2
  * Carapace — OpenClaw Plugin
3
3
  *
4
- * Aggregates upstream MCP servers, enforces Cedar policies on tool access,
5
- * and serves a local GUI for human oversight.
4
+ * Enforces Cedar policies on tool access via OpenClaw's before_tool_call hook.
5
+ * No proxy, no baseUrl redirect, no models.json patching.
6
6
  */
7
7
  import { CedarlingEngine } from "./cedar-engine-cedarling.js";
8
8
  import { McpAggregator } from "./mcp-aggregator.js";
9
9
  import { ControlGui } from "./gui/server.js";
10
- import { LlmProxy } from "./llm-proxy.js";
11
- export { CarapacePolicySource } from "./policy-source.js";
12
10
  export const id = "carapace";
13
11
  export const name = "Carapace";
12
+ /**
13
+ * @deprecated Kept for backward compatibility. No longer used.
14
+ */
15
+ function buildUpstreamConfig(proxyConfig) {
16
+ const upstream = proxyConfig.upstream;
17
+ if (!upstream)
18
+ return {};
19
+ if (typeof upstream === "string") {
20
+ const apiKey = proxyConfig.apiKey ?? "";
21
+ if (upstream.includes("anthropic"))
22
+ return { anthropic: { url: upstream, apiKey } };
23
+ if (upstream.includes("openai"))
24
+ return { openai: { url: upstream, apiKey } };
25
+ return { anthropic: { url: upstream, apiKey } };
26
+ }
27
+ return {
28
+ anthropic: upstream.anthropic ? { url: upstream.anthropic.url ?? "https://api.anthropic.com", apiKey: upstream.anthropic.apiKey } : undefined,
29
+ openai: upstream.openai ? { url: upstream.openai.url ?? "https://api.openai.com", apiKey: upstream.openai.apiKey } : undefined,
30
+ };
31
+ }
32
+ // Audit log
33
+ function appendAuditLog(entry) {
34
+ try {
35
+ const { appendFileSync, mkdirSync } = require("node:fs");
36
+ const { join } = require("node:path");
37
+ const { homedir } = require("node:os");
38
+ const logDir = join(homedir(), ".openclaw", "mcp-policies", "logs");
39
+ mkdirSync(logDir, { recursive: true });
40
+ appendFileSync(join(logDir, "audit.log"), JSON.stringify(entry) + "\n", "utf-8");
41
+ }
42
+ catch { }
43
+ }
14
44
  export default function register(api) {
15
45
  const config = api.pluginConfig ?? {};
16
46
  const logger = api.logger;
@@ -30,103 +60,97 @@ export default function register(api) {
30
60
  aggregator,
31
61
  cedar,
32
62
  logger,
63
+ proxyEnabled: false, // proxy no longer used
33
64
  });
34
- // --- LLM Proxy: intercept tool calls at the API level ---
35
- const proxyConfig = config.proxy;
36
- const proxy = proxyConfig?.enabled ? new LlmProxy({
37
- port: proxyConfig.port ?? 19821,
38
- upstream: {
39
- anthropic: proxyConfig.upstream?.anthropic ? {
40
- url: proxyConfig.upstream.anthropic.url ?? "https://api.anthropic.com",
41
- apiKey: proxyConfig.upstream.anthropic.apiKey,
42
- } : undefined,
43
- openai: proxyConfig.upstream?.openai ? {
44
- url: proxyConfig.upstream.openai.url ?? "https://api.openai.com",
45
- apiKey: proxyConfig.upstream.openai.apiKey,
46
- } : undefined,
47
- },
48
- cedar,
49
- logger,
50
- }) : null;
51
- // --- Bypass detection: warn if built-in tools aren't denied ---
52
- const BYPASS_TOOLS = ["exec", "web_fetch", "web_search"];
53
- function checkForBypasses() {
54
- // Read OpenClaw config to check tools.deny
55
- try {
56
- const { readFileSync, existsSync } = require("node:fs");
57
- const { join } = require("node:path");
58
- const { homedir } = require("node:os");
59
- const configPath = join(homedir(), ".openclaw", "openclaw.json");
60
- if (!existsSync(configPath))
61
- return BYPASS_TOOLS;
62
- const cfg = JSON.parse(readFileSync(configPath, "utf-8"));
63
- const denied = cfg.tools?.deny ?? [];
64
- return BYPASS_TOOLS.filter((t) => !denied.includes(t));
65
- }
66
- catch {
67
- return BYPASS_TOOLS;
68
- }
69
- }
70
- function patchConfigDenyTools() {
71
- const { readFileSync, writeFileSync, existsSync } = require("node:fs");
72
- const { join } = require("node:path");
73
- const { homedir } = require("node:os");
74
- const configPath = join(homedir(), ".openclaw", "openclaw.json");
75
- let cfg = {};
76
- if (existsSync(configPath)) {
77
- cfg = JSON.parse(readFileSync(configPath, "utf-8"));
78
- }
79
- if (!cfg.tools)
80
- cfg.tools = {};
81
- if (!cfg.tools.deny)
82
- cfg.tools.deny = [];
83
- const alreadyDenied = BYPASS_TOOLS.filter((t) => cfg.tools.deny.includes(t));
84
- const toAdd = BYPASS_TOOLS.filter((t) => !cfg.tools.deny.includes(t));
85
- for (const tool of toAdd) {
86
- cfg.tools.deny.push(tool);
87
- }
88
- if (toAdd.length > 0) {
89
- writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
90
- }
91
- return { patched: toAdd, alreadyDenied };
92
- }
93
- function patchConfigProxyBaseUrl() {
94
- const { readFileSync, writeFileSync, existsSync } = require("node:fs");
95
- const { join } = require("node:path");
96
- const { homedir } = require("node:os");
97
- const configPath = join(homedir(), ".openclaw", "openclaw.json");
98
- if (!existsSync(configPath))
99
- return { patched: [], alreadySet: [] };
100
- const cfg = JSON.parse(readFileSync(configPath, "utf-8"));
101
- const port = config.proxy?.port ?? 19821;
102
- const proxyUrl = `http://127.0.0.1:${port}`;
103
- // Figure out which providers have upstream keys configured
104
- const providers = [];
105
- if (config.proxy?.upstream?.anthropic)
106
- providers.push("anthropic");
107
- if (config.proxy?.upstream?.openai)
108
- providers.push("openai");
109
- const patched = [];
110
- const alreadySet = [];
111
- if (!cfg.models)
112
- cfg.models = {};
113
- if (!cfg.models.providers)
114
- cfg.models.providers = {};
115
- for (const provider of providers) {
116
- if (!cfg.models.providers[provider])
117
- cfg.models.providers[provider] = {};
118
- if (cfg.models.providers[provider].baseUrl === proxyUrl) {
119
- alreadySet.push(provider);
65
+ // --- Hook stats ---
66
+ const stats = {
67
+ toolCallsEvaluated: 0,
68
+ toolCallsDenied: 0,
69
+ };
70
+ // --- Register before_tool_call hook ---
71
+ if (api.on) {
72
+ api.on("before_tool_call", async (event) => {
73
+ const toolName = event.toolName ?? event.tool ?? event.name ?? "";
74
+ const params = event.params ?? event.arguments ?? event.input ?? {};
75
+ if (!toolName)
76
+ return {};
77
+ stats.toolCallsEvaluated++;
78
+ // Map tool call to Cedar authorization request
79
+ let resourceType = "Tool";
80
+ let action = "call_tool";
81
+ let resourceId = toolName;
82
+ let context = {};
83
+ // Map known OpenClaw built-in tools to resource types
84
+ if (toolName === "exec" || toolName === "process") {
85
+ resourceType = "Shell";
86
+ action = "exec_command";
87
+ const cmd = params.command ?? "";
88
+ resourceId = cmd.trim().split(/\s+/)[0]?.replace(/^.*\//, "") || toolName;
89
+ context = { args: cmd, workdir: params.workdir ?? "" };
90
+ }
91
+ else if (toolName === "web_fetch" || toolName === "web_search") {
92
+ resourceType = "API";
93
+ action = "call_api";
94
+ const url = params.url ?? params.query ?? "";
95
+ try {
96
+ resourceId = url.startsWith("http") ? new URL(url).hostname : toolName;
97
+ }
98
+ catch {
99
+ resourceId = toolName;
100
+ }
101
+ context = { url, method: params.method ?? "GET", body: params.body ?? "" };
102
+ }
103
+ else if (toolName === "browser") {
104
+ resourceType = "Tool";
105
+ action = "call_tool";
106
+ resourceId = "browser";
107
+ context = { action: params.action ?? "" };
120
108
  }
121
109
  else {
122
- cfg.models.providers[provider].baseUrl = proxyUrl;
123
- patched.push(provider);
110
+ // MCP or other tools
111
+ context = params ? { arguments: params } : {};
124
112
  }
125
- }
126
- if (patched.length > 0) {
127
- writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
128
- }
129
- return { patched, alreadySet };
113
+ // Short-circuit: if default policy is allow-all and no Cedar policies are loaded,
114
+ // skip Cedar evaluation entirely (Cedar is deny-by-default with no policies)
115
+ const effectiveDefault = config.defaultPolicy ?? "allow-all";
116
+ const policies = cedar.getPolicies?.() ?? [];
117
+ if (effectiveDefault === "allow-all" && policies.length === 0) {
118
+ logger.info(`✅ [Carapace] ALLOW (no policies, default allow-all): ${toolName}`);
119
+ return {};
120
+ }
121
+ const decision = await cedar.authorize({
122
+ principal: `Agent::"openclaw"`,
123
+ action: `Action::"${action}"`,
124
+ resource: `${resourceType}::"${resourceId}"`,
125
+ context,
126
+ });
127
+ // If Cedar returns no definitive decision and default is allow-all, allow
128
+ if (effectiveDefault === "allow-all" && decision.decision !== "deny") {
129
+ return {};
130
+ }
131
+ const auditEntry = {
132
+ timestamp: new Date().toISOString(),
133
+ tool: toolName,
134
+ decision: decision.decision,
135
+ reasons: decision.reasons,
136
+ params: Object.keys(params).length > 0 ? params : undefined,
137
+ };
138
+ appendAuditLog(auditEntry);
139
+ if (decision.decision === "deny") {
140
+ stats.toolCallsDenied++;
141
+ const reason = `Cedar policy denied: ${toolName} (${decision.reasons.join(", ") || "default deny"})`;
142
+ logger.info(`🚫 ${reason}`);
143
+ return { block: true, blockReason: reason };
144
+ }
145
+ return {};
146
+ }, {
147
+ name: "carapace.cedar-enforcement",
148
+ description: "Cedar policy enforcement for all tool calls",
149
+ });
150
+ logger.info("Registered before_tool_call hook for Cedar policy enforcement");
151
+ }
152
+ else {
153
+ logger.warn("⚠️ before_tool_call hook not available — Cedar policies will NOT be enforced on built-in tools");
130
154
  }
131
155
  // --- Background service: connect to MCP servers and serve GUI ---
132
156
  api.registerService({
@@ -137,24 +161,16 @@ export default function register(api) {
137
161
  await aggregator.connectAll();
138
162
  await gui.start();
139
163
  logger.info(`Control GUI at http://localhost:${config.guiPort ?? 19820}`);
140
- if (proxy) {
141
- await proxy.start();
142
- logger.info(`🛡️ LLM Proxy active on http://127.0.0.1:${proxyConfig.port ?? 19821} — ` +
143
- `all tool calls go through Cedar`);
144
- }
145
- else {
146
- // Check for bypass vulnerabilities only when proxy is disabled
147
- const bypasses = checkForBypasses();
148
- if (bypasses.length > 0) {
149
- logger.warn(`⚠️ BYPASS RISK: Built-in tools [${bypasses.join(", ")}] are NOT denied and LLM proxy is not enabled. ` +
150
- `Agents can use these to bypass Carapace Cedar policies. ` +
151
- `Enable the LLM proxy (recommended) or run "openclaw carapace setup" to deny built-in tools.`);
152
- }
164
+ // Warn if no policies are loaded
165
+ const policies = cedar.getPolicies();
166
+ if (policies.length === 0) {
167
+ logger.warn(`⚠️ Carapace is loaded but NOT ENFORCING — no Cedar policies found. ` +
168
+ `Add policies to ${config.policyDir ?? "~/.openclaw/mcp-policies/"} or use the GUI at http://localhost:${config.guiPort ?? 19820}`);
153
169
  }
170
+ logger.info(`🛡️ Cedar enforcement active via before_tool_call hook — ` +
171
+ `${policies.length} policies loaded, default: ${config.defaultPolicy ?? "allow-all"}`);
154
172
  },
155
173
  async stop() {
156
- if (proxy)
157
- await proxy.stop();
158
174
  await gui.stop();
159
175
  await aggregator.disconnectAll();
160
176
  logger.info("Carapace stopped");
@@ -177,16 +193,11 @@ export default function register(api) {
177
193
  async execute(_toolCallId, params) {
178
194
  const tools = aggregator.listTools(params.server);
179
195
  return {
180
- content: [
181
- {
182
- type: "text",
183
- text: JSON.stringify(tools, null, 2),
184
- },
185
- ],
196
+ content: [{ type: "text", text: JSON.stringify(tools, null, 2) }],
186
197
  };
187
198
  },
188
199
  });
189
- // --- Agent tool: invoke an MCP tool through the proxy ---
200
+ // --- Agent tool: invoke an MCP tool through Cedar ---
190
201
  api.registerTool({
191
202
  name: "mcp_call",
192
203
  label: "MCP Call (Carapace)",
@@ -207,7 +218,6 @@ export default function register(api) {
207
218
  },
208
219
  async execute(_toolCallId, params) {
209
220
  const { tool, arguments: args } = params;
210
- // Authorize via Cedar
211
221
  const decision = await cedar.authorize({
212
222
  principal: 'Agent::"openclaw"',
213
223
  action: 'Action::"call_tool"',
@@ -216,48 +226,31 @@ export default function register(api) {
216
226
  });
217
227
  if (decision.decision === "deny") {
218
228
  return {
219
- content: [
220
- {
221
- type: "text",
222
- text: `DENIED by Cedar policy: ${tool}\nReason: ${decision.reasons.join(", ") || "default deny"}`,
223
- },
224
- ],
229
+ content: [{ type: "text", text: `DENIED by Cedar policy: ${tool}\nReason: ${decision.reasons.join(", ") || "default deny"}` }],
225
230
  isError: true,
226
231
  };
227
232
  }
228
- // Forward to upstream MCP server
229
233
  const result = await aggregator.callTool(tool, args ?? {});
230
234
  return result;
231
235
  },
232
236
  });
233
- // --- Agent tool: execute a shell command through Cedar authorization ---
237
+ // --- Agent tool: Cedar-gated shell exec ---
234
238
  api.registerTool({
235
239
  name: "carapace_exec",
236
240
  label: "Shell Exec (Carapace)",
237
- description: "Execute a shell command through the Carapace Cedar proxy. The command is authorized by Cedar policies before execution. Use this when you want Cedar-gated shell access.",
241
+ description: "Execute a shell command through the Carapace Cedar proxy. The command is authorized by Cedar policies before execution.",
238
242
  parameters: {
239
243
  type: "object",
240
244
  required: ["command"],
241
245
  properties: {
242
- command: {
243
- type: "string",
244
- description: "The shell command to execute (e.g., 'git status', 'npm install')",
245
- },
246
- workdir: {
247
- type: "string",
248
- description: "Working directory for the command (optional)",
249
- },
250
- timeout: {
251
- type: "number",
252
- description: "Timeout in seconds (default: 30)",
253
- },
246
+ command: { type: "string", description: "The shell command to execute" },
247
+ workdir: { type: "string", description: "Working directory (optional)" },
248
+ timeout: { type: "number", description: "Timeout in seconds (default: 30)" },
254
249
  },
255
250
  },
256
251
  async execute(_toolCallId, params) {
257
252
  const { command, workdir, timeout = 30 } = params;
258
- // Extract the binary name for policy matching
259
253
  const binary = command.trim().split(/\s+/)[0].replace(/^.*\//, "");
260
- // Authorize via Cedar
261
254
  const decision = await cedar.authorize({
262
255
  principal: `Agent::"openclaw"`,
263
256
  action: `Action::"exec_command"`,
@@ -266,16 +259,10 @@ export default function register(api) {
266
259
  });
267
260
  if (decision.decision === "deny") {
268
261
  return {
269
- content: [
270
- {
271
- type: "text",
272
- text: `DENIED by Cedar policy: shell command "${binary}"\nFull command: ${command}\nReason: ${decision.reasons.join(", ") || "default deny"}`,
273
- },
274
- ],
262
+ content: [{ type: "text", text: `DENIED by Cedar policy: shell command "${binary}"\nFull command: ${command}\nReason: ${decision.reasons.join(", ") || "default deny"}` }],
275
263
  isError: true,
276
264
  };
277
265
  }
278
- // Execute the command
279
266
  try {
280
267
  const { execSync } = await import("node:child_process");
281
268
  const result = execSync(command, {
@@ -285,64 +272,41 @@ export default function register(api) {
285
272
  encoding: "utf-8",
286
273
  stdio: ["pipe", "pipe", "pipe"],
287
274
  });
288
- return {
289
- content: [{ type: "text", text: result }],
290
- };
275
+ return { content: [{ type: "text", text: result }] };
291
276
  }
292
277
  catch (err) {
293
- const output = err.stdout ?? err.stderr ?? err.message;
294
278
  return {
295
- content: [{ type: "text", text: `Command failed (exit ${err.status ?? "?"}): ${output}` }],
279
+ content: [{ type: "text", text: `Command failed (exit ${err.status ?? "?"}): ${err.stdout ?? err.stderr ?? err.message}` }],
296
280
  isError: true,
297
281
  };
298
282
  }
299
283
  },
300
284
  }, { optional: true });
301
- // --- Agent tool: make an HTTP API call through Cedar authorization ---
285
+ // --- Agent tool: Cedar-gated HTTP fetch ---
302
286
  api.registerTool({
303
287
  name: "carapace_fetch",
304
288
  label: "API Fetch (Carapace)",
305
- description: "Make an HTTP API call through the Carapace Cedar proxy. The request is authorized by Cedar policies before being sent. Use this when you want Cedar-gated outbound API access.",
289
+ description: "Make an HTTP API call through Carapace Cedar authorization.",
306
290
  parameters: {
307
291
  type: "object",
308
292
  required: ["url"],
309
293
  properties: {
310
- url: {
311
- type: "string",
312
- description: "The URL to fetch",
313
- },
314
- method: {
315
- type: "string",
316
- description: "HTTP method (GET, POST, PUT, DELETE, PATCH). Default: GET",
317
- },
318
- headers: {
319
- type: "object",
320
- description: "HTTP headers to include",
321
- },
322
- body: {
323
- type: "string",
324
- description: "Request body (for POST/PUT/PATCH)",
325
- },
326
- timeout: {
327
- type: "number",
328
- description: "Timeout in seconds (default: 30)",
329
- },
294
+ url: { type: "string", description: "The URL to fetch" },
295
+ method: { type: "string", description: "HTTP method (default: GET)" },
296
+ headers: { type: "object", description: "HTTP headers" },
297
+ body: { type: "string", description: "Request body" },
298
+ timeout: { type: "number", description: "Timeout in seconds (default: 30)" },
330
299
  },
331
300
  },
332
301
  async execute(_toolCallId, params) {
333
302
  const { url, method = "GET", headers = {}, body, timeout = 30 } = params;
334
- // Extract domain for policy matching
335
303
  let domain;
336
304
  try {
337
305
  domain = new URL(url).hostname;
338
306
  }
339
307
  catch {
340
- return {
341
- content: [{ type: "text", text: `Invalid URL: ${url}` }],
342
- isError: true,
343
- };
308
+ return { content: [{ type: "text", text: `Invalid URL: ${url}` }], isError: true };
344
309
  }
345
- // Authorize via Cedar
346
310
  const decision = await cedar.authorize({
347
311
  principal: `Agent::"openclaw"`,
348
312
  action: `Action::"call_api"`,
@@ -351,51 +315,27 @@ export default function register(api) {
351
315
  });
352
316
  if (decision.decision === "deny") {
353
317
  return {
354
- content: [
355
- {
356
- type: "text",
357
- text: `DENIED by Cedar policy: API call to "${domain}"\nURL: ${url}\nMethod: ${method}\nReason: ${decision.reasons.join(", ") || "default deny"}`,
358
- },
359
- ],
318
+ content: [{ type: "text", text: `DENIED by Cedar policy: API call to "${domain}"\nURL: ${url}\nReason: ${decision.reasons.join(", ") || "default deny"}` }],
360
319
  isError: true,
361
320
  };
362
321
  }
363
- // Make the HTTP request
364
322
  try {
365
323
  const controller = new AbortController();
366
324
  const timer = setTimeout(() => controller.abort(), timeout * 1000);
367
- const response = await fetch(url, {
368
- method,
369
- headers,
370
- body: body ?? undefined,
371
- signal: controller.signal,
372
- });
325
+ const response = await fetch(url, { method, headers, body: body ?? undefined, signal: controller.signal });
373
326
  clearTimeout(timer);
374
327
  const responseText = await response.text();
375
- const truncated = responseText.length > 50000
376
- ? responseText.slice(0, 50000) + "\n...[truncated]"
377
- : responseText;
378
- return {
379
- content: [
380
- {
381
- type: "text",
382
- text: `HTTP ${response.status} ${response.statusText}\n\n${truncated}`,
383
- },
384
- ],
385
- isError: !response.ok,
386
- };
328
+ const truncated = responseText.length > 50000 ? responseText.slice(0, 50000) + "\n...[truncated]" : responseText;
329
+ return { content: [{ type: "text", text: `HTTP ${response.status} ${response.statusText}\n\n${truncated}` }], isError: !response.ok };
387
330
  }
388
331
  catch (err) {
389
- return {
390
- content: [{ type: "text", text: `API call failed: ${err.message}` }],
391
- isError: true,
392
- };
332
+ return { content: [{ type: "text", text: `API call failed: ${err.message}` }], isError: true };
393
333
  }
394
334
  },
395
335
  }, { optional: true });
396
- // --- CLI command ---
336
+ // --- CLI commands ---
397
337
  api.registerCli?.(({ program }) => {
398
- const cmd = program.command("carapace").description("Carapace — MCP tool authorization");
338
+ const cmd = program.command("carapace").description("Carapace — Cedar policy enforcement for agent tools");
399
339
  cmd.command("status").action(async () => {
400
340
  const servers = aggregator.getServerStatus();
401
341
  console.log("\n🦞 Carapace Status\n");
@@ -407,14 +347,11 @@ export default function register(api) {
407
347
  const enabled = tools.filter((t) => t.enabled).length;
408
348
  console.log(`\n ${enabled}/${tools.length} tools enabled`);
409
349
  console.log(` GUI: http://localhost:${config.guiPort ?? 19820}`);
410
- if (proxy) {
411
- const stats = proxy.getStats();
412
- console.log(`\n 🛡️ LLM Proxy: http://127.0.0.1:${proxyConfig.port ?? 19821}`);
413
- console.log(` Requests: ${stats.requests} | Tool calls evaluated: ${stats.toolCallsEvaluated} | Denied: ${stats.toolCallsDenied}`);
414
- }
415
- else {
416
- console.log(`\n ⚠️ LLM Proxy: disabled`);
417
- }
350
+ const policies = cedar.getPolicies();
351
+ console.log(`\n 🛡️ Enforcement: before_tool_call hook`);
352
+ console.log(` Policies: ${policies.length} loaded`);
353
+ console.log(` Default: ${config.defaultPolicy ?? "allow-all"}`);
354
+ console.log(` Evaluated: ${stats.toolCallsEvaluated} | Denied: ${stats.toolCallsDenied}`);
418
355
  console.log();
419
356
  });
420
357
  cmd.command("tools").action(async () => {
@@ -431,146 +368,94 @@ export default function register(api) {
431
368
  }
432
369
  else {
433
370
  console.log("⚠️ Verification issues:");
434
- for (const issue of result.issues) {
371
+ for (const issue of result.issues)
435
372
  console.log(` - ${issue}`);
436
- }
437
373
  }
438
374
  });
439
375
  cmd.command("setup")
440
- .description("Configure OpenClaw to route all traffic through Carapace")
376
+ .description("Enable Carapace plugin in OpenClaw config")
441
377
  .action(async () => {
378
+ const { readFileSync, writeFileSync, existsSync } = require("node:fs");
379
+ const { join } = require("node:path");
380
+ const { homedir } = require("node:os");
442
381
  console.log("\n🦞 Carapace Setup\n");
443
- let anyChanges = false;
444
- // 1. Deny built-in bypass tools
445
- const bypasses = checkForBypasses();
446
- if (bypasses.length > 0) {
447
- console.log(" Denying built-in tools that bypass Cedar:");
448
- const { patched, alreadyDenied } = patchConfigDenyTools();
449
- if (alreadyDenied.length > 0) {
450
- console.log(` Already denied: ${alreadyDenied.join(", ")}`);
451
- }
452
- if (patched.length > 0) {
453
- console.log(` ✅ Added to tools.deny: ${patched.join(", ")}`);
454
- anyChanges = true;
455
- }
382
+ const configPath = join(homedir(), ".openclaw", "openclaw.json");
383
+ let cfg = {};
384
+ if (existsSync(configPath)) {
385
+ cfg = JSON.parse(readFileSync(configPath, "utf-8"));
456
386
  }
457
- else {
458
- console.log(" ✅ Built-in bypass tools already denied.");
387
+ if (!cfg.plugins)
388
+ cfg.plugins = {};
389
+ if (!cfg.plugins.entries)
390
+ cfg.plugins.entries = {};
391
+ if (!cfg.plugins.entries.carapace)
392
+ cfg.plugins.entries.carapace = {};
393
+ const alreadyEnabled = cfg.plugins.entries.carapace.enabled === true;
394
+ cfg.plugins.entries.carapace.enabled = true;
395
+ if (!cfg.plugins.entries.carapace.config) {
396
+ cfg.plugins.entries.carapace.config = {
397
+ defaultPolicy: "allow-all",
398
+ };
459
399
  }
460
- // 2. Set up LLM proxy baseUrl if proxy is configured
461
- if (config.proxy?.enabled) {
462
- console.log("\n Configuring LLM proxy baseUrl:");
463
- const { patched, alreadySet } = patchConfigProxyBaseUrl();
464
- if (alreadySet.length > 0) {
465
- console.log(` Already set: ${alreadySet.join(", ")}`);
466
- }
467
- if (patched.length > 0) {
468
- console.log(` ✅ Set models.providers baseUrl for: ${patched.join(", ")}`);
469
- anyChanges = true;
470
- }
471
- if (patched.length === 0 && alreadySet.length === 0) {
472
- console.log(" ⚠️ No upstream providers configured in proxy config.");
473
- console.log(" Add proxy.upstream.anthropic or proxy.upstream.openai to your plugin config.");
474
- }
400
+ writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
401
+ if (alreadyEnabled) {
402
+ console.log(" Carapace already enabled. No changes needed.\n");
475
403
  }
476
404
  else {
477
- console.log("\n LLM proxy not enabled skipping baseUrl setup.");
478
- console.log(" To enable, add proxy.enabled: true to your Carapace plugin config.");
479
- }
480
- if (anyChanges) {
405
+ console.log(" Enabled carapace plugin in openclaw.json");
406
+ console.log(" 🛡️ Cedar policies enforced via before_tool_call hook");
407
+ console.log(" 📋 No models.json or baseUrl changes needed");
481
408
  console.log("\n Restart the gateway for changes to take effect:");
482
409
  console.log(" openclaw gateway restart\n");
483
410
  }
484
- else {
485
- console.log("\n ✅ Everything already configured. No changes needed.\n");
486
- }
487
411
  });
488
412
  cmd.command("uninstall")
489
- .description("Reverse all config changes made by Carapace (restores built-in tools)")
413
+ .description("Disable Carapace plugin")
490
414
  .action(async () => {
415
+ const { readFileSync, writeFileSync, existsSync } = require("node:fs");
416
+ const { join } = require("node:path");
417
+ const { homedir } = require("node:os");
491
418
  console.log("\n🦞 Carapace Uninstall\n");
492
- console.log(" This reverses changes made by 'openclaw carapace setup'.\n");
493
- try {
494
- const { readFileSync, writeFileSync, existsSync } = require("node:fs");
495
- const { join } = require("node:path");
496
- const { homedir } = require("node:os");
497
- const configPath = join(homedir(), ".openclaw", "openclaw.json");
498
- if (!existsSync(configPath)) {
499
- console.log(" No config file found. Nothing to undo.\n");
500
- return;
501
- }
502
- const cfg = JSON.parse(readFileSync(configPath, "utf-8"));
503
- let changed = false;
504
- // Remove Carapace-added entries from tools.deny
505
- if (cfg.tools?.deny) {
506
- const before = cfg.tools.deny.length;
507
- cfg.tools.deny = cfg.tools.deny.filter((t) => !BYPASS_TOOLS.includes(t));
508
- if (cfg.tools.deny.length === 0)
509
- delete cfg.tools.deny;
510
- if (cfg.tools && Object.keys(cfg.tools).length === 0)
511
- delete cfg.tools;
512
- if (cfg.tools?.deny?.length !== before) {
513
- changed = true;
514
- console.log(` ✅ Removed [${BYPASS_TOOLS.join(", ")}] from tools.deny`);
515
- console.log(" Built-in exec, web_fetch, and web_search are restored.");
516
- }
517
- }
518
- // Remove models.providers baseUrl override if it points at the proxy
519
- const proxyPort = cfg.plugins?.entries?.carapace?.config?.proxy?.port ?? 19821;
520
- const proxyUrl = `http://127.0.0.1:${proxyPort}`;
521
- if (cfg.models?.providers) {
522
- for (const [name, provCfg] of Object.entries(cfg.models.providers)) {
523
- if (provCfg?.baseUrl === proxyUrl) {
524
- delete provCfg.baseUrl;
525
- // Clean up empty objects
526
- if (Object.keys(provCfg).length === 0)
527
- delete cfg.models.providers[name];
528
- changed = true;
529
- console.log(` ✅ Removed baseUrl proxy override for ${name}`);
530
- console.log(` ${name} will connect directly to its API again.`);
531
- }
532
- }
533
- if (Object.keys(cfg.models.providers).length === 0)
534
- delete cfg.models.providers;
535
- if (cfg.models && Object.keys(cfg.models).length === 0)
536
- delete cfg.models;
537
- }
538
- // Disable the plugin entry (don't delete — user might want to re-enable)
539
- if (cfg.plugins?.entries?.carapace?.enabled) {
540
- cfg.plugins.entries.carapace.enabled = false;
541
- changed = true;
542
- console.log(" ✅ Disabled carapace plugin in config");
543
- }
544
- if (changed) {
545
- writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
546
- console.log("\n Config updated. Restart the gateway for changes to take effect:");
547
- console.log(" openclaw gateway restart\n");
548
- console.log(" To fully remove the plugin files:");
549
- console.log(" rm -rf ~/.openclaw/extensions/carapace\n");
550
- }
551
- else {
552
- console.log(" No Carapace changes found in config. Nothing to undo.\n");
553
- }
419
+ const configPath = join(homedir(), ".openclaw", "openclaw.json");
420
+ if (!existsSync(configPath)) {
421
+ console.log(" No config file found. Nothing to undo.\n");
422
+ return;
554
423
  }
555
- catch (err) {
556
- console.log(` ❌ Error: ${err.message}\n`);
424
+ const cfg = JSON.parse(readFileSync(configPath, "utf-8"));
425
+ let changed = false;
426
+ if (cfg.plugins?.entries?.carapace?.enabled) {
427
+ cfg.plugins.entries.carapace.enabled = false;
428
+ changed = true;
429
+ console.log(" ✅ Disabled carapace plugin");
430
+ }
431
+ if (changed) {
432
+ writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
433
+ console.log("\n Restart the gateway for changes to take effect:");
434
+ console.log(" openclaw gateway restart\n");
435
+ console.log(" To fully remove the plugin files:");
436
+ console.log(" rm -rf ~/.openclaw/extensions/carapace\n");
437
+ }
438
+ else {
439
+ console.log(" Carapace already disabled. Nothing to undo.\n");
557
440
  }
558
441
  });
559
442
  cmd.command("check")
560
- .description("Check for bypass vulnerabilities (built-in tools that skip Cedar)")
443
+ .description("Check Cedar policy status")
561
444
  .action(async () => {
562
445
  console.log("\n🦞 Carapace Security Check\n");
563
- const bypasses = checkForBypasses();
564
- if (bypasses.length === 0) {
565
- console.log(" No bypass vulnerabilities found.");
566
- console.log(" All agent exec/fetch operations go through Cedar.\n");
446
+ const policies = cedar.getPolicies();
447
+ if (policies.length === 0) {
448
+ console.log(" ⚠️ No Cedar policies loaded.");
449
+ console.log(` Add policies to ${config.policyDir ?? "~/.openclaw/mcp-policies/"}\n`);
567
450
  }
568
451
  else {
569
- console.log(" ⚠️ Bypass vulnerabilities found:\n");
570
- for (const tool of bypasses) {
571
- console.log(` 🔓 ${tool} agents can bypass Cedar policies via this tool`);
452
+ console.log(` ${policies.length} Cedar policies loaded`);
453
+ console.log(` 🛡️ Enforcement via before_tool_call hook`);
454
+ console.log(` Default: ${config.defaultPolicy ?? "allow-all"}\n`);
455
+ for (const p of policies) {
456
+ console.log(` ${p.effect === "permit" ? "🟢" : "🔴"} ${p.id}`);
572
457
  }
573
- console.log(`\n Run "openclaw carapace setup" to fix.\n`);
458
+ console.log();
574
459
  }
575
460
  });
576
461
  }, { commands: ["carapace"] });
@@ -578,7 +463,15 @@ export default function register(api) {
578
463
  api.registerGatewayMethod?.("carapace.status", ({ respond }) => {
579
464
  const servers = aggregator.getServerStatus();
580
465
  const tools = aggregator.listTools();
581
- respond(true, { servers, toolCount: tools.length, enabledCount: tools.filter((t) => t.enabled).length });
466
+ const policies = cedar.getPolicies();
467
+ respond(true, {
468
+ servers,
469
+ toolCount: tools.length,
470
+ enabledCount: tools.filter((t) => t.enabled).length,
471
+ policyCount: policies.length,
472
+ enforcement: "before_tool_call",
473
+ stats,
474
+ });
582
475
  });
583
476
  }
584
477
  //# sourceMappingURL=index.js.map