@clawdreyhepburn/carapace 1.0.5 → 1.0.7

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,58 @@
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
+ import { appendFileSync, mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { homedir } from "node:os";
7
10
  import { CedarlingEngine } from "./cedar-engine-cedarling.js";
8
11
  import { McpAggregator } from "./mcp-aggregator.js";
9
12
  import { ControlGui } from "./gui/server.js";
10
- import { LlmProxy } from "./llm-proxy.js";
11
- export { CarapacePolicySource } from "./policy-source.js";
12
13
  export const id = "carapace";
13
14
  export const name = "Carapace";
15
+ /**
16
+ * Resolve the OpenClaw home directory the same way the runtime does:
17
+ * honor $OPENCLAW_HOME when set, otherwise fall back to ~/.openclaw.
18
+ * Used by the audit log and the setup/uninstall CLI so they never
19
+ * read/write the wrong config for users with a custom OPENCLAW_HOME.
20
+ */
21
+ function openclawHome() {
22
+ const override = process.env.OPENCLAW_HOME;
23
+ if (override && override.trim() !== "")
24
+ return override;
25
+ return join(homedir(), ".openclaw");
26
+ }
27
+ /**
28
+ * @deprecated Kept for backward compatibility. No longer used.
29
+ */
30
+ function buildUpstreamConfig(proxyConfig) {
31
+ const upstream = proxyConfig.upstream;
32
+ if (!upstream)
33
+ return {};
34
+ if (typeof upstream === "string") {
35
+ const apiKey = proxyConfig.apiKey ?? "";
36
+ if (upstream.includes("anthropic"))
37
+ return { anthropic: { url: upstream, apiKey } };
38
+ if (upstream.includes("openai"))
39
+ return { openai: { url: upstream, apiKey } };
40
+ return { anthropic: { url: upstream, apiKey } };
41
+ }
42
+ return {
43
+ anthropic: upstream.anthropic ? { url: upstream.anthropic.url ?? "https://api.anthropic.com", apiKey: upstream.anthropic.apiKey } : undefined,
44
+ openai: upstream.openai ? { url: upstream.openai.url ?? "https://api.openai.com", apiKey: upstream.openai.apiKey } : undefined,
45
+ };
46
+ }
47
+ // Audit log
48
+ function appendAuditLog(entry) {
49
+ try {
50
+ const logDir = join(openclawHome(), "mcp-policies", "logs");
51
+ mkdirSync(logDir, { recursive: true });
52
+ appendFileSync(join(logDir, "audit.log"), JSON.stringify(entry) + "\n", "utf-8");
53
+ }
54
+ catch { }
55
+ }
14
56
  export default function register(api) {
15
57
  const config = api.pluginConfig ?? {};
16
58
  const logger = api.logger;
@@ -30,103 +72,97 @@ export default function register(api) {
30
72
  aggregator,
31
73
  cedar,
32
74
  logger,
75
+ proxyEnabled: false, // proxy no longer used
33
76
  });
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);
77
+ // --- Hook stats ---
78
+ const stats = {
79
+ toolCallsEvaluated: 0,
80
+ toolCallsDenied: 0,
81
+ };
82
+ // --- Register before_tool_call hook ---
83
+ if (api.on) {
84
+ api.on("before_tool_call", async (event) => {
85
+ const toolName = event.toolName ?? event.tool ?? event.name ?? "";
86
+ const params = event.params ?? event.arguments ?? event.input ?? {};
87
+ if (!toolName)
88
+ return {};
89
+ stats.toolCallsEvaluated++;
90
+ // Map tool call to Cedar authorization request
91
+ let resourceType = "Tool";
92
+ let action = "call_tool";
93
+ let resourceId = toolName;
94
+ let context = {};
95
+ // Map known OpenClaw built-in tools to resource types
96
+ if (toolName === "exec" || toolName === "process") {
97
+ resourceType = "Shell";
98
+ action = "exec_command";
99
+ const cmd = params.command ?? "";
100
+ resourceId = cmd.trim().split(/\s+/)[0]?.replace(/^.*\//, "") || toolName;
101
+ context = { args: cmd, workdir: params.workdir ?? "" };
102
+ }
103
+ else if (toolName === "web_fetch" || toolName === "web_search") {
104
+ resourceType = "API";
105
+ action = "call_api";
106
+ const url = params.url ?? params.query ?? "";
107
+ try {
108
+ resourceId = url.startsWith("http") ? new URL(url).hostname : toolName;
109
+ }
110
+ catch {
111
+ resourceId = toolName;
112
+ }
113
+ context = { url, method: params.method ?? "GET", body: params.body ?? "" };
114
+ }
115
+ else if (toolName === "browser") {
116
+ resourceType = "Tool";
117
+ action = "call_tool";
118
+ resourceId = "browser";
119
+ context = { action: params.action ?? "" };
120
120
  }
121
121
  else {
122
- cfg.models.providers[provider].baseUrl = proxyUrl;
123
- patched.push(provider);
122
+ // MCP or other tools
123
+ context = params ? { arguments: params } : {};
124
124
  }
125
- }
126
- if (patched.length > 0) {
127
- writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
128
- }
129
- return { patched, alreadySet };
125
+ // Short-circuit: if default policy is allow-all and no Cedar policies are loaded,
126
+ // skip Cedar evaluation entirely (Cedar is deny-by-default with no policies)
127
+ const effectiveDefault = config.defaultPolicy ?? "allow-all";
128
+ const policies = cedar.getPolicies?.() ?? [];
129
+ if (effectiveDefault === "allow-all" && policies.length === 0) {
130
+ logger.info(`✅ [Carapace] ALLOW (no policies, default allow-all): ${toolName}`);
131
+ return {};
132
+ }
133
+ const decision = await cedar.authorize({
134
+ principal: `Agent::"openclaw"`,
135
+ action: `Action::"${action}"`,
136
+ resource: `${resourceType}::"${resourceId}"`,
137
+ context,
138
+ });
139
+ // If Cedar returns no definitive decision and default is allow-all, allow
140
+ if (effectiveDefault === "allow-all" && decision.decision !== "deny") {
141
+ return {};
142
+ }
143
+ const auditEntry = {
144
+ timestamp: new Date().toISOString(),
145
+ tool: toolName,
146
+ decision: decision.decision,
147
+ reasons: decision.reasons,
148
+ params: Object.keys(params).length > 0 ? params : undefined,
149
+ };
150
+ appendAuditLog(auditEntry);
151
+ if (decision.decision === "deny") {
152
+ stats.toolCallsDenied++;
153
+ const reason = `Cedar policy denied: ${toolName} (${decision.reasons.join(", ") || "default deny"})`;
154
+ logger.info(`🚫 ${reason}`);
155
+ return { block: true, blockReason: reason };
156
+ }
157
+ return {};
158
+ }, {
159
+ name: "carapace.cedar-enforcement",
160
+ description: "Cedar policy enforcement for all tool calls",
161
+ });
162
+ logger.info("Registered before_tool_call hook for Cedar policy enforcement");
163
+ }
164
+ else {
165
+ logger.warn("⚠️ before_tool_call hook not available — Cedar policies will NOT be enforced on built-in tools");
130
166
  }
131
167
  // --- Background service: connect to MCP servers and serve GUI ---
132
168
  api.registerService({
@@ -137,24 +173,16 @@ export default function register(api) {
137
173
  await aggregator.connectAll();
138
174
  await gui.start();
139
175
  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
- }
176
+ // Warn if no policies are loaded
177
+ const policies = cedar.getPolicies();
178
+ if (policies.length === 0) {
179
+ logger.warn(`⚠️ Carapace is loaded but NOT ENFORCING — no Cedar policies found. ` +
180
+ `Add policies to ${config.policyDir ?? "~/.openclaw/mcp-policies/"} or use the GUI at http://localhost:${config.guiPort ?? 19820}`);
153
181
  }
182
+ logger.info(`🛡️ Cedar enforcement active via before_tool_call hook — ` +
183
+ `${policies.length} policies loaded, default: ${config.defaultPolicy ?? "allow-all"}`);
154
184
  },
155
185
  async stop() {
156
- if (proxy)
157
- await proxy.stop();
158
186
  await gui.stop();
159
187
  await aggregator.disconnectAll();
160
188
  logger.info("Carapace stopped");
@@ -177,16 +205,11 @@ export default function register(api) {
177
205
  async execute(_toolCallId, params) {
178
206
  const tools = aggregator.listTools(params.server);
179
207
  return {
180
- content: [
181
- {
182
- type: "text",
183
- text: JSON.stringify(tools, null, 2),
184
- },
185
- ],
208
+ content: [{ type: "text", text: JSON.stringify(tools, null, 2) }],
186
209
  };
187
210
  },
188
211
  });
189
- // --- Agent tool: invoke an MCP tool through the proxy ---
212
+ // --- Agent tool: invoke an MCP tool through Cedar ---
190
213
  api.registerTool({
191
214
  name: "mcp_call",
192
215
  label: "MCP Call (Carapace)",
@@ -207,7 +230,6 @@ export default function register(api) {
207
230
  },
208
231
  async execute(_toolCallId, params) {
209
232
  const { tool, arguments: args } = params;
210
- // Authorize via Cedar
211
233
  const decision = await cedar.authorize({
212
234
  principal: 'Agent::"openclaw"',
213
235
  action: 'Action::"call_tool"',
@@ -216,48 +238,31 @@ export default function register(api) {
216
238
  });
217
239
  if (decision.decision === "deny") {
218
240
  return {
219
- content: [
220
- {
221
- type: "text",
222
- text: `DENIED by Cedar policy: ${tool}\nReason: ${decision.reasons.join(", ") || "default deny"}`,
223
- },
224
- ],
241
+ content: [{ type: "text", text: `DENIED by Cedar policy: ${tool}\nReason: ${decision.reasons.join(", ") || "default deny"}` }],
225
242
  isError: true,
226
243
  };
227
244
  }
228
- // Forward to upstream MCP server
229
245
  const result = await aggregator.callTool(tool, args ?? {});
230
246
  return result;
231
247
  },
232
248
  });
233
- // --- Agent tool: execute a shell command through Cedar authorization ---
249
+ // --- Agent tool: Cedar-gated shell exec ---
234
250
  api.registerTool({
235
251
  name: "carapace_exec",
236
252
  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.",
253
+ description: "Execute a shell command through the Carapace Cedar proxy. The command is authorized by Cedar policies before execution.",
238
254
  parameters: {
239
255
  type: "object",
240
256
  required: ["command"],
241
257
  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
- },
258
+ command: { type: "string", description: "The shell command to execute" },
259
+ workdir: { type: "string", description: "Working directory (optional)" },
260
+ timeout: { type: "number", description: "Timeout in seconds (default: 30)" },
254
261
  },
255
262
  },
256
263
  async execute(_toolCallId, params) {
257
264
  const { command, workdir, timeout = 30 } = params;
258
- // Extract the binary name for policy matching
259
265
  const binary = command.trim().split(/\s+/)[0].replace(/^.*\//, "");
260
- // Authorize via Cedar
261
266
  const decision = await cedar.authorize({
262
267
  principal: `Agent::"openclaw"`,
263
268
  action: `Action::"exec_command"`,
@@ -266,16 +271,10 @@ export default function register(api) {
266
271
  });
267
272
  if (decision.decision === "deny") {
268
273
  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
- ],
274
+ content: [{ type: "text", text: `DENIED by Cedar policy: shell command "${binary}"\nFull command: ${command}\nReason: ${decision.reasons.join(", ") || "default deny"}` }],
275
275
  isError: true,
276
276
  };
277
277
  }
278
- // Execute the command
279
278
  try {
280
279
  const { execSync } = await import("node:child_process");
281
280
  const result = execSync(command, {
@@ -285,64 +284,41 @@ export default function register(api) {
285
284
  encoding: "utf-8",
286
285
  stdio: ["pipe", "pipe", "pipe"],
287
286
  });
288
- return {
289
- content: [{ type: "text", text: result }],
290
- };
287
+ return { content: [{ type: "text", text: result }] };
291
288
  }
292
289
  catch (err) {
293
- const output = err.stdout ?? err.stderr ?? err.message;
294
290
  return {
295
- content: [{ type: "text", text: `Command failed (exit ${err.status ?? "?"}): ${output}` }],
291
+ content: [{ type: "text", text: `Command failed (exit ${err.status ?? "?"}): ${err.stdout ?? err.stderr ?? err.message}` }],
296
292
  isError: true,
297
293
  };
298
294
  }
299
295
  },
300
296
  }, { optional: true });
301
- // --- Agent tool: make an HTTP API call through Cedar authorization ---
297
+ // --- Agent tool: Cedar-gated HTTP fetch ---
302
298
  api.registerTool({
303
299
  name: "carapace_fetch",
304
300
  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.",
301
+ description: "Make an HTTP API call through Carapace Cedar authorization.",
306
302
  parameters: {
307
303
  type: "object",
308
304
  required: ["url"],
309
305
  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
- },
306
+ url: { type: "string", description: "The URL to fetch" },
307
+ method: { type: "string", description: "HTTP method (default: GET)" },
308
+ headers: { type: "object", description: "HTTP headers" },
309
+ body: { type: "string", description: "Request body" },
310
+ timeout: { type: "number", description: "Timeout in seconds (default: 30)" },
330
311
  },
331
312
  },
332
313
  async execute(_toolCallId, params) {
333
314
  const { url, method = "GET", headers = {}, body, timeout = 30 } = params;
334
- // Extract domain for policy matching
335
315
  let domain;
336
316
  try {
337
317
  domain = new URL(url).hostname;
338
318
  }
339
319
  catch {
340
- return {
341
- content: [{ type: "text", text: `Invalid URL: ${url}` }],
342
- isError: true,
343
- };
320
+ return { content: [{ type: "text", text: `Invalid URL: ${url}` }], isError: true };
344
321
  }
345
- // Authorize via Cedar
346
322
  const decision = await cedar.authorize({
347
323
  principal: `Agent::"openclaw"`,
348
324
  action: `Action::"call_api"`,
@@ -351,51 +327,27 @@ export default function register(api) {
351
327
  });
352
328
  if (decision.decision === "deny") {
353
329
  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
- ],
330
+ content: [{ type: "text", text: `DENIED by Cedar policy: API call to "${domain}"\nURL: ${url}\nReason: ${decision.reasons.join(", ") || "default deny"}` }],
360
331
  isError: true,
361
332
  };
362
333
  }
363
- // Make the HTTP request
364
334
  try {
365
335
  const controller = new AbortController();
366
336
  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
- });
337
+ const response = await fetch(url, { method, headers, body: body ?? undefined, signal: controller.signal });
373
338
  clearTimeout(timer);
374
339
  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
- };
340
+ const truncated = responseText.length > 50000 ? responseText.slice(0, 50000) + "\n...[truncated]" : responseText;
341
+ return { content: [{ type: "text", text: `HTTP ${response.status} ${response.statusText}\n\n${truncated}` }], isError: !response.ok };
387
342
  }
388
343
  catch (err) {
389
- return {
390
- content: [{ type: "text", text: `API call failed: ${err.message}` }],
391
- isError: true,
392
- };
344
+ return { content: [{ type: "text", text: `API call failed: ${err.message}` }], isError: true };
393
345
  }
394
346
  },
395
347
  }, { optional: true });
396
- // --- CLI command ---
348
+ // --- CLI commands ---
397
349
  api.registerCli?.(({ program }) => {
398
- const cmd = program.command("carapace").description("Carapace — MCP tool authorization");
350
+ const cmd = program.command("carapace").description("Carapace — Cedar policy enforcement for agent tools");
399
351
  cmd.command("status").action(async () => {
400
352
  const servers = aggregator.getServerStatus();
401
353
  console.log("\n🦞 Carapace Status\n");
@@ -407,14 +359,11 @@ export default function register(api) {
407
359
  const enabled = tools.filter((t) => t.enabled).length;
408
360
  console.log(`\n ${enabled}/${tools.length} tools enabled`);
409
361
  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
- }
362
+ const policies = cedar.getPolicies();
363
+ console.log(`\n 🛡️ Enforcement: before_tool_call hook`);
364
+ console.log(` Policies: ${policies.length} loaded`);
365
+ console.log(` Default: ${config.defaultPolicy ?? "allow-all"}`);
366
+ console.log(` Evaluated: ${stats.toolCallsEvaluated} | Denied: ${stats.toolCallsDenied}`);
418
367
  console.log();
419
368
  });
420
369
  cmd.command("tools").action(async () => {
@@ -431,146 +380,88 @@ export default function register(api) {
431
380
  }
432
381
  else {
433
382
  console.log("⚠️ Verification issues:");
434
- for (const issue of result.issues) {
383
+ for (const issue of result.issues)
435
384
  console.log(` - ${issue}`);
436
- }
437
385
  }
438
386
  });
439
387
  cmd.command("setup")
440
- .description("Configure OpenClaw to route all traffic through Carapace")
388
+ .description("Enable Carapace plugin in OpenClaw config")
441
389
  .action(async () => {
442
390
  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
- }
391
+ const configPath = join(openclawHome(), "openclaw.json");
392
+ let cfg = {};
393
+ if (existsSync(configPath)) {
394
+ cfg = JSON.parse(readFileSync(configPath, "utf-8"));
456
395
  }
457
- else {
458
- console.log(" ✅ Built-in bypass tools already denied.");
396
+ if (!cfg.plugins)
397
+ cfg.plugins = {};
398
+ if (!cfg.plugins.entries)
399
+ cfg.plugins.entries = {};
400
+ if (!cfg.plugins.entries.carapace)
401
+ cfg.plugins.entries.carapace = {};
402
+ const alreadyEnabled = cfg.plugins.entries.carapace.enabled === true;
403
+ cfg.plugins.entries.carapace.enabled = true;
404
+ if (!cfg.plugins.entries.carapace.config) {
405
+ cfg.plugins.entries.carapace.config = {
406
+ defaultPolicy: "allow-all",
407
+ };
459
408
  }
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
- }
409
+ writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
410
+ if (alreadyEnabled) {
411
+ console.log(" Carapace already enabled. No changes needed.\n");
475
412
  }
476
413
  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) {
414
+ console.log(" Enabled carapace plugin in openclaw.json");
415
+ console.log(" 🛡️ Cedar policies enforced via before_tool_call hook");
416
+ console.log(" 📋 No models.json or baseUrl changes needed");
481
417
  console.log("\n Restart the gateway for changes to take effect:");
482
418
  console.log(" openclaw gateway restart\n");
483
419
  }
484
- else {
485
- console.log("\n ✅ Everything already configured. No changes needed.\n");
486
- }
487
420
  });
488
421
  cmd.command("uninstall")
489
- .description("Reverse all config changes made by Carapace (restores built-in tools)")
422
+ .description("Disable Carapace plugin")
490
423
  .action(async () => {
491
424
  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
- }
425
+ const configPath = join(openclawHome(), "openclaw.json");
426
+ if (!existsSync(configPath)) {
427
+ console.log(" No config file found. Nothing to undo.\n");
428
+ return;
554
429
  }
555
- catch (err) {
556
- console.log(` ❌ Error: ${err.message}\n`);
430
+ const cfg = JSON.parse(readFileSync(configPath, "utf-8"));
431
+ let changed = false;
432
+ if (cfg.plugins?.entries?.carapace?.enabled) {
433
+ cfg.plugins.entries.carapace.enabled = false;
434
+ changed = true;
435
+ console.log(" ✅ Disabled carapace plugin");
436
+ }
437
+ if (changed) {
438
+ writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
439
+ console.log("\n Restart the gateway for changes to take effect:");
440
+ console.log(" openclaw gateway restart\n");
441
+ console.log(" To fully remove the plugin files:");
442
+ console.log(" rm -rf ~/.openclaw/extensions/carapace\n");
443
+ }
444
+ else {
445
+ console.log(" Carapace already disabled. Nothing to undo.\n");
557
446
  }
558
447
  });
559
448
  cmd.command("check")
560
- .description("Check for bypass vulnerabilities (built-in tools that skip Cedar)")
449
+ .description("Check Cedar policy status")
561
450
  .action(async () => {
562
451
  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");
452
+ const policies = cedar.getPolicies();
453
+ if (policies.length === 0) {
454
+ console.log(" ⚠️ No Cedar policies loaded.");
455
+ console.log(` Add policies to ${config.policyDir ?? "~/.openclaw/mcp-policies/"}\n`);
567
456
  }
568
457
  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`);
458
+ console.log(` ${policies.length} Cedar policies loaded`);
459
+ console.log(` 🛡️ Enforcement via before_tool_call hook`);
460
+ console.log(` Default: ${config.defaultPolicy ?? "allow-all"}\n`);
461
+ for (const p of policies) {
462
+ console.log(` ${p.effect === "permit" ? "🟢" : "🔴"} ${p.id}`);
572
463
  }
573
- console.log(`\n Run "openclaw carapace setup" to fix.\n`);
464
+ console.log();
574
465
  }
575
466
  });
576
467
  }, { commands: ["carapace"] });
@@ -578,7 +469,15 @@ export default function register(api) {
578
469
  api.registerGatewayMethod?.("carapace.status", ({ respond }) => {
579
470
  const servers = aggregator.getServerStatus();
580
471
  const tools = aggregator.listTools();
581
- respond(true, { servers, toolCount: tools.length, enabledCount: tools.filter((t) => t.enabled).length });
472
+ const policies = cedar.getPolicies();
473
+ respond(true, {
474
+ servers,
475
+ toolCount: tools.length,
476
+ enabledCount: tools.filter((t) => t.enabled).length,
477
+ policyCount: policies.length,
478
+ enforcement: "before_tool_call",
479
+ stats,
480
+ });
582
481
  });
583
482
  }
584
483
  //# sourceMappingURL=index.js.map