@clawdreyhepburn/carapace 0.5.0 → 1.0.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.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "id": "carapace",
3
3
  "name": "Carapace",
4
- "description": "Immutable policy boundaries for MCP tool access. Your agent's exoskeleton.",
5
- "version": "0.5.0",
4
+ "description": "Cedar policy enforcement for agent tool access via before_tool_call hook. Your agent's exoskeleton.",
5
+ "version": "1.0.0",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": true,
@@ -20,31 +20,13 @@
20
20
  "properties": {
21
21
  "transport": {
22
22
  "type": "string",
23
- "enum": [
24
- "stdio",
25
- "http",
26
- "sse"
27
- ],
23
+ "enum": ["stdio", "http", "sse"],
28
24
  "default": "stdio"
29
25
  },
30
- "command": {
31
- "type": "string"
32
- },
33
- "args": {
34
- "type": "array",
35
- "items": {
36
- "type": "string"
37
- }
38
- },
39
- "env": {
40
- "type": "object",
41
- "additionalProperties": {
42
- "type": "string"
43
- }
44
- },
45
- "url": {
46
- "type": "string"
47
- }
26
+ "command": { "type": "string" },
27
+ "args": { "type": "array", "items": { "type": "string" } },
28
+ "env": { "type": "object", "additionalProperties": { "type": "string" } },
29
+ "url": { "type": "string" }
48
30
  }
49
31
  }
50
32
  },
@@ -55,72 +37,21 @@
55
37
  },
56
38
  "defaultPolicy": {
57
39
  "type": "string",
58
- "enum": [
59
- "deny-all",
60
- "allow-all"
61
- ],
40
+ "enum": ["deny-all", "allow-all"],
62
41
  "default": "allow-all",
63
- "description": "Default policy for tools. allow-all (default) keeps everything working — use the GUI to restrict. deny-all requires explicit permits."
42
+ "description": "Default policy when no Cedar policies match. allow-all passes everything through; deny-all blocks everything not explicitly permitted."
64
43
  },
65
44
  "verify": {
66
45
  "type": "boolean",
67
46
  "default": false,
68
47
  "description": "Run cvc5 formal verification on policy changes"
69
- },
70
- "proxy": {
71
- "type": "object",
72
- "description": "LLM proxy settings — intercepts tool_use blocks and enforces Cedar policies before OpenClaw sees them",
73
- "properties": {
74
- "enabled": {
75
- "type": "boolean",
76
- "default": false,
77
- "description": "Enable the LLM API proxy"
78
- },
79
- "port": {
80
- "type": "number",
81
- "default": 19821,
82
- "description": "Port for the LLM proxy server"
83
- },
84
- "upstream": {
85
- "type": "string",
86
- "default": "https://api.anthropic.com",
87
- "description": "Upstream LLM API base URL"
88
- },
89
- "apiKey": {
90
- "type": "string",
91
- "description": "API key for the upstream provider (moved here so the agent never sees it)"
92
- }
93
- }
94
48
  }
95
49
  }
96
50
  },
97
51
  "uiHints": {
98
- "guiPort": {
99
- "label": "GUI Port",
100
- "placeholder": "19820"
101
- },
102
- "policyDir": {
103
- "label": "Policy Directory"
104
- },
105
- "defaultPolicy": {
106
- "label": "Default Policy for New Tools"
107
- },
108
- "verify": {
109
- "label": "Enable Formal Verification"
110
- },
111
- "proxy.enabled": {
112
- "label": "Enable LLM Proxy"
113
- },
114
- "proxy.port": {
115
- "label": "Proxy Port",
116
- "placeholder": "19821"
117
- },
118
- "proxy.upstream": {
119
- "label": "Upstream API URL"
120
- },
121
- "proxy.apiKey": {
122
- "label": "Upstream API Key",
123
- "sensitive": true
124
- }
52
+ "guiPort": { "label": "GUI Port", "placeholder": "19820" },
53
+ "policyDir": { "label": "Policy Directory" },
54
+ "defaultPolicy": { "label": "Default Policy" },
55
+ "verify": { "label": "Enable Formal Verification" }
125
56
  }
126
57
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@clawdreyhepburn/carapace",
3
- "version": "0.5.0",
4
- "description": "Immutable policy boundaries for MCP tool access. Powered by Cedar + Cedarling WASM.",
3
+ "version": "1.0.1",
4
+ "description": "Cedar policy enforcement for agent tool access via OpenClaw's before_tool_call hook.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "openclaw": {
package/src/index.ts CHANGED
@@ -1,14 +1,13 @@
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
 
8
8
  import { CedarlingEngine } from "./cedar-engine-cedarling.js";
9
9
  import { McpAggregator } from "./mcp-aggregator.js";
10
10
  import { ControlGui } from "./gui/server.js";
11
- import { LlmProxy } from "./llm-proxy.js";
12
11
  import type { PluginConfig } from "./types.js";
13
12
 
14
13
  export const id = "carapace";
@@ -16,7 +15,6 @@ export const name = "Carapace";
16
15
 
17
16
  /**
18
17
  * OpenClaw plugin API shape (matches real runtime).
19
- * We define it here to avoid depending on OpenClaw types at build time.
20
18
  */
21
19
  interface OpenClawPluginApi {
22
20
  pluginConfig: any;
@@ -37,50 +35,44 @@ interface OpenClawPluginApi {
37
35
  },
38
36
  opts?: { optional?: boolean },
39
37
  ): void;
38
+ registerHook?(hookName: string | string[], handler: (event: any) => Promise<any> | any, opts?: { name: string; description?: string; priority?: number }): void;
40
39
  registerCli?(fn: (ctx: { program: any }) => void, opts?: { commands: string[] }): void;
41
40
  registerGatewayMethod?(name: string, handler: (ctx: { respond: (ok: boolean, data: any) => void }) => void): void;
42
41
  }
43
42
 
44
43
  /**
45
- * Build upstream config from either string or object format.
46
- * String format: proxy.upstream = "https://api.anthropic.com", proxy.apiKey = "sk-..."
47
- * Object format: proxy.upstream = { anthropic: { url, apiKey }, openai: { url, apiKey } }
44
+ * @deprecated Kept for backward compatibility. No longer used.
48
45
  */
49
46
  function buildUpstreamConfig(proxyConfig: NonNullable<PluginConfig["proxy"]>): {
50
47
  anthropic?: { url: string; apiKey: string };
51
48
  openai?: { url: string; apiKey: string };
52
49
  } {
53
50
  const upstream = proxyConfig.upstream;
54
-
55
51
  if (!upstream) return {};
56
-
57
- // String format: single upstream URL + flat apiKey
58
52
  if (typeof upstream === "string") {
59
53
  const apiKey = proxyConfig.apiKey ?? "";
60
- const url = upstream;
61
- // Guess provider from URL
62
- if (url.includes("anthropic")) {
63
- return { anthropic: { url, apiKey } };
64
- } else if (url.includes("openai")) {
65
- return { openai: { url, apiKey } };
66
- }
67
- // Default to anthropic
68
- return { anthropic: { url, apiKey } };
54
+ if (upstream.includes("anthropic")) return { anthropic: { url: upstream, apiKey } };
55
+ if (upstream.includes("openai")) return { openai: { url: upstream, apiKey } };
56
+ return { anthropic: { url: upstream, apiKey } };
69
57
  }
70
-
71
- // Object format: multi-provider
72
58
  return {
73
- anthropic: upstream.anthropic ? {
74
- url: upstream.anthropic.url ?? "https://api.anthropic.com",
75
- apiKey: upstream.anthropic.apiKey,
76
- } : undefined,
77
- openai: upstream.openai ? {
78
- url: upstream.openai.url ?? "https://api.openai.com",
79
- apiKey: upstream.openai.apiKey,
80
- } : undefined,
59
+ anthropic: upstream.anthropic ? { url: upstream.anthropic.url ?? "https://api.anthropic.com", apiKey: upstream.anthropic.apiKey } : undefined,
60
+ openai: upstream.openai ? { url: upstream.openai.url ?? "https://api.openai.com", apiKey: upstream.openai.apiKey } : undefined,
81
61
  };
82
62
  }
83
63
 
64
+ // Audit log
65
+ function appendAuditLog(entry: { timestamp: string; tool: string; decision: string; reasons: string[]; params?: any }): void {
66
+ try {
67
+ const { appendFileSync, mkdirSync } = require("node:fs");
68
+ const { join } = require("node:path");
69
+ const { homedir } = require("node:os");
70
+ const logDir = join(homedir(), ".openclaw", "mcp-policies", "logs");
71
+ mkdirSync(logDir, { recursive: true });
72
+ appendFileSync(join(logDir, "audit.log"), JSON.stringify(entry) + "\n", "utf-8");
73
+ } catch {}
74
+ }
75
+
84
76
  export default function register(api: OpenClawPluginApi) {
85
77
  const config: PluginConfig = api.pluginConfig ?? {};
86
78
  const logger = api.logger;
@@ -98,172 +90,91 @@ export default function register(api: OpenClawPluginApi) {
98
90
  logger,
99
91
  });
100
92
 
101
- // --- LLM Proxy: intercept tool calls at the API level ---
102
- const proxyConfig = config.proxy;
103
-
104
93
  const gui = new ControlGui({
105
94
  port: config.guiPort ?? 19820,
106
95
  aggregator,
107
96
  cedar,
108
97
  logger,
109
- proxyEnabled: !!proxyConfig?.enabled,
98
+ proxyEnabled: false, // proxy no longer used
110
99
  });
111
100
 
112
- let proxy: LlmProxy | null = proxyConfig?.enabled ? new LlmProxy({
113
- port: proxyConfig.port ?? 19821,
114
- upstream: buildUpstreamConfig(proxyConfig),
115
- cedar,
116
- logger,
117
- }) : null;
118
-
119
- // --- Bypass detection: warn if built-in tools aren't denied ---
120
- const BYPASS_TOOLS = ["exec", "web_fetch", "web_search"];
121
-
122
- function checkForBypasses(): string[] {
123
- // Read OpenClaw config to check tools.deny
124
- try {
125
- const { readFileSync, existsSync } = require("node:fs");
126
- const { join } = require("node:path");
127
- const { homedir } = require("node:os");
128
- const configPath = join(homedir(), ".openclaw", "openclaw.json");
129
- if (!existsSync(configPath)) return BYPASS_TOOLS;
130
- const cfg = JSON.parse(readFileSync(configPath, "utf-8"));
131
- const denied: string[] = cfg.tools?.deny ?? [];
132
- return BYPASS_TOOLS.filter((t) => !denied.includes(t));
133
- } catch {
134
- return BYPASS_TOOLS;
135
- }
136
- }
137
-
138
- function patchConfigDenyTools(): { patched: string[]; alreadyDenied: string[] } {
139
- const { readFileSync, writeFileSync, existsSync } = require("node:fs");
140
- const { join } = require("node:path");
141
- const { homedir } = require("node:os");
142
- const configPath = join(homedir(), ".openclaw", "openclaw.json");
143
-
144
- let cfg: any = {};
145
- if (existsSync(configPath)) {
146
- cfg = JSON.parse(readFileSync(configPath, "utf-8"));
147
- }
148
-
149
- if (!cfg.tools) cfg.tools = {};
150
- if (!cfg.tools.deny) cfg.tools.deny = [];
151
-
152
- const alreadyDenied = BYPASS_TOOLS.filter((t) => cfg.tools.deny.includes(t));
153
- const toAdd = BYPASS_TOOLS.filter((t) => !cfg.tools.deny.includes(t));
154
-
155
- for (const tool of toAdd) {
156
- cfg.tools.deny.push(tool);
157
- }
158
-
159
- if (toAdd.length > 0) {
160
- writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
161
- }
162
-
163
- return { patched: toAdd, alreadyDenied };
164
- }
165
-
166
- function backupConfig(): void {
167
- const { readFileSync, writeFileSync, existsSync, copyFileSync } = require("node:fs");
168
- const { join } = require("node:path");
169
- const { homedir } = require("node:os");
170
- const configPath = join(homedir(), ".openclaw", "openclaw.json");
171
- if (existsSync(configPath)) {
172
- const backupPath = configPath + ".carapace-backup";
173
- copyFileSync(configPath, backupPath);
174
- }
175
- }
176
-
177
- function patchConfigProxyBaseUrl(): { patched: string[]; alreadySet: string[] } {
178
- const { readFileSync, writeFileSync, existsSync } = require("node:fs");
179
- const { join } = require("node:path");
180
- const { homedir } = require("node:os");
181
- const configPath = join(homedir(), ".openclaw", "openclaw.json");
182
-
183
- if (!existsSync(configPath)) return { patched: [], alreadySet: [] };
184
- const cfg = JSON.parse(readFileSync(configPath, "utf-8"));
185
-
186
- const port = config.proxy?.port ?? 19821;
187
- const proxyUrl = `http://127.0.0.1:${port}`;
101
+ // --- Hook stats ---
102
+ const stats = {
103
+ toolCallsEvaluated: 0,
104
+ toolCallsDenied: 0,
105
+ };
188
106
 
189
- // Figure out which providers are configured
190
- const upstreamConfig = proxyConfig ? buildUpstreamConfig(proxyConfig) : {};
191
- const providers = Object.keys(upstreamConfig).filter(
192
- (k) => upstreamConfig[k as keyof typeof upstreamConfig],
193
- );
107
+ // --- Register before_tool_call hook ---
108
+ if (api.registerHook) {
109
+ api.registerHook("before_tool_call", async (event: any) => {
110
+ const toolName: string = event.toolName ?? event.tool ?? event.name ?? "";
111
+ const params: Record<string, unknown> = event.params ?? event.arguments ?? event.input ?? {};
112
+
113
+ if (!toolName) return {};
114
+
115
+ stats.toolCallsEvaluated++;
116
+
117
+ // Map tool call to Cedar authorization request
118
+ let resourceType = "Tool";
119
+ let action = "call_tool";
120
+ let resourceId = toolName;
121
+ let context: Record<string, unknown> = {};
122
+
123
+ // Map known OpenClaw built-in tools to resource types
124
+ if (toolName === "exec" || toolName === "process") {
125
+ resourceType = "Shell";
126
+ action = "exec_command";
127
+ const cmd = (params.command as string) ?? "";
128
+ resourceId = cmd.trim().split(/\s+/)[0]?.replace(/^.*\//, "") || toolName;
129
+ context = { args: cmd, workdir: (params.workdir as string) ?? "" };
130
+ } else if (toolName === "web_fetch" || toolName === "web_search") {
131
+ resourceType = "API";
132
+ action = "call_api";
133
+ const url = (params.url as string) ?? (params.query as string) ?? "";
134
+ try {
135
+ resourceId = url.startsWith("http") ? new URL(url).hostname : toolName;
136
+ } catch {
137
+ resourceId = toolName;
138
+ }
139
+ context = { url, method: (params.method as string) ?? "GET", body: (params.body as string) ?? "" };
140
+ } else if (toolName === "browser") {
141
+ resourceType = "Tool";
142
+ action = "call_tool";
143
+ resourceId = "browser";
144
+ context = { action: (params.action as string) ?? "" };
145
+ } else {
146
+ // MCP or other tools
147
+ context = params ? { arguments: params } : {};
148
+ }
194
149
 
195
- const patched: string[] = [];
196
- const alreadySet: string[] = [];
150
+ const decision = await cedar.authorize({
151
+ principal: `Agent::"openclaw"`,
152
+ action: `Action::"${action}"`,
153
+ resource: `${resourceType}::"${resourceId}"`,
154
+ context,
155
+ });
197
156
 
198
- if (!cfg.models) cfg.models = {};
199
- if (!cfg.models.mode) cfg.models.mode = "merge";
200
- if (!cfg.models.providers) cfg.models.providers = {};
157
+ const auditEntry = {
158
+ timestamp: new Date().toISOString(),
159
+ tool: toolName,
160
+ decision: decision.decision,
161
+ reasons: decision.reasons,
162
+ params: Object.keys(params).length > 0 ? params : undefined,
163
+ };
164
+ appendAuditLog(auditEntry);
201
165
 
202
- for (const provider of providers) {
203
- if (!cfg.models.providers[provider]) cfg.models.providers[provider] = {};
204
- // Ensure models array exists (OpenClaw requires it)
205
- if (!Array.isArray(cfg.models.providers[provider].models)) {
206
- cfg.models.providers[provider].models = [];
207
- }
208
- if (cfg.models.providers[provider].baseUrl === proxyUrl) {
209
- alreadySet.push(provider);
210
- } else {
211
- // Store original baseUrl for clean revert
212
- if (cfg.models.providers[provider].baseUrl && cfg.models.providers[provider].baseUrl !== proxyUrl) {
213
- cfg.models.providers[provider]._originalBaseUrl = cfg.models.providers[provider].baseUrl;
214
- }
215
- cfg.models.providers[provider].baseUrl = proxyUrl;
216
- patched.push(provider);
217
- }
218
- }
219
-
220
- // Ensure plugin config is under plugins.entries.carapace.config
221
- if (!cfg.plugins) cfg.plugins = {};
222
- if (!cfg.plugins.entries) cfg.plugins.entries = {};
223
- if (!cfg.plugins.entries.carapace) cfg.plugins.entries.carapace = {};
224
- if (!cfg.plugins.entries.carapace.config) cfg.plugins.entries.carapace.config = {};
225
-
226
- if (patched.length > 0 || !cfg.plugins.entries.carapace.enabled) {
227
- cfg.plugins.entries.carapace.enabled = true;
228
- writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
229
- }
230
-
231
- // Also patch the per-agent models.json (this is what OpenClaw actually reads at runtime)
232
- // openclaw.json models.providers gets merged INTO models.json on restart,
233
- // but if someone restores openclaw.json from backup, models.json keeps the stale baseUrl.
234
- // So we patch both files for safety.
235
- const agentModelsPath = join(homedir(), ".openclaw", "agents", "main", "agent", "models.json");
236
- if (existsSync(agentModelsPath)) {
237
- try {
238
- const agentModels = JSON.parse(readFileSync(agentModelsPath, "utf-8"));
239
- let agentModelsChanged = false;
240
- if (!agentModels.providers) agentModels.providers = {};
241
- for (const provider of providers) {
242
- if (!agentModels.providers[provider]) agentModels.providers[provider] = {};
243
- if (agentModels.providers[provider].baseUrl !== proxyUrl) {
244
- if (agentModels.providers[provider].baseUrl && agentModels.providers[provider].baseUrl !== proxyUrl) {
245
- agentModels.providers[provider]._originalBaseUrl = agentModels.providers[provider].baseUrl;
246
- }
247
- agentModels.providers[provider].baseUrl = proxyUrl;
248
- agentModelsChanged = true;
249
- }
250
- }
251
- if (agentModelsChanged) {
252
- // Backup models.json before modifying
253
- const modelsBackup = agentModelsPath + ".carapace-backup";
254
- if (!existsSync(modelsBackup)) {
255
- const { copyFileSync } = require("node:fs");
256
- copyFileSync(agentModelsPath, modelsBackup);
257
- }
258
- writeFileSync(agentModelsPath, JSON.stringify(agentModels, null, 2) + "\n", "utf-8");
259
- patched.push(...providers.map(p => `models.json:${p}`));
260
- }
261
- } catch (e: any) {
262
- logger.warn(`Failed to patch agent models.json: ${e.message}`);
166
+ if (decision.decision === "deny") {
167
+ stats.toolCallsDenied++;
168
+ const reason = `Cedar policy denied: ${toolName} (${decision.reasons.join(", ") || "default deny"})`;
169
+ logger.info(`🚫 ${reason}`);
170
+ return { block: true, blockReason: reason };
263
171
  }
264
- }
265
172
 
266
- return { patched, alreadySet };
173
+ return {};
174
+ });
175
+ logger.info("Registered before_tool_call hook for Cedar policy enforcement");
176
+ } else {
177
+ logger.warn("⚠️ before_tool_call hook not available — Cedar policies will NOT be enforced on built-in tools");
267
178
  }
268
179
 
269
180
  // --- Background service: connect to MCP servers and serve GUI ---
@@ -276,54 +187,21 @@ export default function register(api: OpenClawPluginApi) {
276
187
  await gui.start();
277
188
  logger.info(`Control GUI at http://localhost:${config.guiPort ?? 19820}`);
278
189
 
279
- if (proxy) {
280
- await proxy.start();
281
-
282
- // Health check: verify proxy is actually responding
283
- const proxyPort = proxyConfig!.port ?? 19821;
284
- try {
285
- const controller = new AbortController();
286
- const timer = setTimeout(() => controller.abort(), 3000);
287
- const healthResp = await fetch(`http://127.0.0.1:${proxyPort}/health`, { signal: controller.signal });
288
- clearTimeout(timer);
289
- if (!healthResp.ok) throw new Error(`HTTP ${healthResp.status}`);
290
- } catch (err: any) {
291
- logger.error(`❌ Proxy health check failed on port ${proxyPort}: ${err.message}. Disabling proxy.`);
292
- try { await proxy.stop(); } catch {}
293
- proxy = null;
294
- }
295
-
296
- if (proxy) {
297
- logger.info(
298
- `🛡️ LLM Proxy active on http://127.0.0.1:${proxyPort} — ` +
299
- `all tool calls go through Cedar`
300
- );
301
- }
302
- } else {
303
- // Check for bypass vulnerabilities only when proxy is disabled
304
- const bypasses = checkForBypasses();
305
- if (bypasses.length > 0) {
306
- logger.warn(
307
- `⚠️ BYPASS RISK: Built-in tools [${bypasses.join(", ")}] are NOT denied and LLM proxy is not enabled. ` +
308
- `Agents can use these to bypass Carapace Cedar policies. ` +
309
- `Enable the LLM proxy (recommended) or run "openclaw carapace setup" to deny built-in tools.`
310
- );
311
- }
312
- }
313
-
314
- // Warn if Carapace is loaded but not actually enforcing anything
315
- const tools = aggregator.listTools();
316
- const enabledCount = tools.filter((t: any) => t.enabled).length;
317
- if (!proxy && enabledCount === 0) {
190
+ // Warn if no policies are loaded
191
+ const policies = cedar.getPolicies();
192
+ if (policies.length === 0) {
318
193
  logger.warn(
319
- `⚠️ Carapace is loaded but NOT ENFORCING. No tools are gated and the LLM proxy is disabled. ` +
320
- `Your agent is running without policy protection. ` +
321
- `Run "openclaw carapace setup" to activate enforcement, or configure policies at http://localhost:${config.guiPort ?? 19820}`
194
+ `⚠️ Carapace is loaded but NOT ENFORCING no Cedar policies found. ` +
195
+ `Add policies to ${config.policyDir ?? "~/.openclaw/mcp-policies/"} or use the GUI at http://localhost:${config.guiPort ?? 19820}`
322
196
  );
323
197
  }
198
+
199
+ logger.info(
200
+ `🛡️ Cedar enforcement active via before_tool_call hook — ` +
201
+ `${policies.length} policies loaded, default: ${config.defaultPolicy ?? "allow-all"}`
202
+ );
324
203
  },
325
204
  async stop() {
326
- if (proxy) await proxy.stop();
327
205
  await gui.stop();
328
206
  await aggregator.disconnectAll();
329
207
  logger.info("Carapace stopped");
@@ -348,17 +226,12 @@ export default function register(api: OpenClawPluginApi) {
348
226
  async execute(_toolCallId: string, params: { server?: string }) {
349
227
  const tools = aggregator.listTools(params.server);
350
228
  return {
351
- content: [
352
- {
353
- type: "text",
354
- text: JSON.stringify(tools, null, 2),
355
- },
356
- ],
229
+ content: [{ type: "text", text: JSON.stringify(tools, null, 2) }],
357
230
  };
358
231
  },
359
232
  });
360
233
 
361
- // --- Agent tool: invoke an MCP tool through the proxy ---
234
+ // --- Agent tool: invoke an MCP tool through Cedar ---
362
235
  api.registerTool({
363
236
  name: "mcp_call",
364
237
  label: "MCP Call (Carapace)",
@@ -380,8 +253,6 @@ export default function register(api: OpenClawPluginApi) {
380
253
  },
381
254
  async execute(_toolCallId: string, params: { tool: string; arguments?: Record<string, unknown> }) {
382
255
  const { tool, arguments: args } = params;
383
-
384
- // Authorize via Cedar
385
256
  const decision = await cedar.authorize({
386
257
  principal: 'Agent::"openclaw"',
387
258
  action: 'Action::"call_tool"',
@@ -391,53 +262,35 @@ export default function register(api: OpenClawPluginApi) {
391
262
 
392
263
  if (decision.decision === "deny") {
393
264
  return {
394
- content: [
395
- {
396
- type: "text",
397
- text: `DENIED by Cedar policy: ${tool}\nReason: ${decision.reasons.join(", ") || "default deny"}`,
398
- },
399
- ],
265
+ content: [{ type: "text", text: `DENIED by Cedar policy: ${tool}\nReason: ${decision.reasons.join(", ") || "default deny"}` }],
400
266
  isError: true,
401
267
  };
402
268
  }
403
269
 
404
- // Forward to upstream MCP server
405
270
  const result = await aggregator.callTool(tool, args ?? {});
406
271
  return result;
407
272
  },
408
273
  });
409
274
 
410
- // --- Agent tool: execute a shell command through Cedar authorization ---
275
+ // --- Agent tool: Cedar-gated shell exec ---
411
276
  api.registerTool({
412
277
  name: "carapace_exec",
413
278
  label: "Shell Exec (Carapace)",
414
279
  description:
415
- "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.",
280
+ "Execute a shell command through the Carapace Cedar proxy. The command is authorized by Cedar policies before execution.",
416
281
  parameters: {
417
282
  type: "object",
418
283
  required: ["command"],
419
284
  properties: {
420
- command: {
421
- type: "string",
422
- description: "The shell command to execute (e.g., 'git status', 'npm install')",
423
- },
424
- workdir: {
425
- type: "string",
426
- description: "Working directory for the command (optional)",
427
- },
428
- timeout: {
429
- type: "number",
430
- description: "Timeout in seconds (default: 30)",
431
- },
285
+ command: { type: "string", description: "The shell command to execute" },
286
+ workdir: { type: "string", description: "Working directory (optional)" },
287
+ timeout: { type: "number", description: "Timeout in seconds (default: 30)" },
432
288
  },
433
289
  },
434
290
  async execute(_toolCallId: string, params: { command: string; workdir?: string; timeout?: number }) {
435
291
  const { command, workdir, timeout = 30 } = params;
436
-
437
- // Extract the binary name for policy matching
438
292
  const binary = command.trim().split(/\s+/)[0].replace(/^.*\//, "");
439
293
 
440
- // Authorize via Cedar
441
294
  const decision = await cedar.authorize({
442
295
  principal: `Agent::"openclaw"`,
443
296
  action: `Action::"exec_command"`,
@@ -447,17 +300,11 @@ export default function register(api: OpenClawPluginApi) {
447
300
 
448
301
  if (decision.decision === "deny") {
449
302
  return {
450
- content: [
451
- {
452
- type: "text",
453
- text: `DENIED by Cedar policy: shell command "${binary}"\nFull command: ${command}\nReason: ${decision.reasons.join(", ") || "default deny"}`,
454
- },
455
- ],
303
+ content: [{ type: "text", text: `DENIED by Cedar policy: shell command "${binary}"\nFull command: ${command}\nReason: ${decision.reasons.join(", ") || "default deny"}` }],
456
304
  isError: true,
457
305
  };
458
306
  }
459
307
 
460
- // Execute the command
461
308
  try {
462
309
  const { execSync } = await import("node:child_process");
463
310
  const result = execSync(command, {
@@ -467,49 +314,31 @@ export default function register(api: OpenClawPluginApi) {
467
314
  encoding: "utf-8",
468
315
  stdio: ["pipe", "pipe", "pipe"],
469
316
  });
470
- return {
471
- content: [{ type: "text", text: result }],
472
- };
317
+ return { content: [{ type: "text", text: result }] };
473
318
  } catch (err: any) {
474
- const output = err.stdout ?? err.stderr ?? err.message;
475
319
  return {
476
- content: [{ type: "text", text: `Command failed (exit ${err.status ?? "?"}): ${output}` }],
320
+ content: [{ type: "text", text: `Command failed (exit ${err.status ?? "?"}): ${err.stdout ?? err.stderr ?? err.message}` }],
477
321
  isError: true,
478
322
  };
479
323
  }
480
324
  },
481
325
  }, { optional: true });
482
326
 
483
- // --- Agent tool: make an HTTP API call through Cedar authorization ---
327
+ // --- Agent tool: Cedar-gated HTTP fetch ---
484
328
  api.registerTool({
485
329
  name: "carapace_fetch",
486
330
  label: "API Fetch (Carapace)",
487
331
  description:
488
- "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.",
332
+ "Make an HTTP API call through Carapace Cedar authorization.",
489
333
  parameters: {
490
334
  type: "object",
491
335
  required: ["url"],
492
336
  properties: {
493
- url: {
494
- type: "string",
495
- description: "The URL to fetch",
496
- },
497
- method: {
498
- type: "string",
499
- description: "HTTP method (GET, POST, PUT, DELETE, PATCH). Default: GET",
500
- },
501
- headers: {
502
- type: "object",
503
- description: "HTTP headers to include",
504
- },
505
- body: {
506
- type: "string",
507
- description: "Request body (for POST/PUT/PATCH)",
508
- },
509
- timeout: {
510
- type: "number",
511
- description: "Timeout in seconds (default: 30)",
512
- },
337
+ url: { type: "string", description: "The URL to fetch" },
338
+ method: { type: "string", description: "HTTP method (default: GET)" },
339
+ headers: { type: "object", description: "HTTP headers" },
340
+ body: { type: "string", description: "Request body" },
341
+ timeout: { type: "number", description: "Timeout in seconds (default: 30)" },
513
342
  },
514
343
  },
515
344
  async execute(_toolCallId: string, params: {
@@ -517,18 +346,11 @@ export default function register(api: OpenClawPluginApi) {
517
346
  }) {
518
347
  const { url, method = "GET", headers = {}, body, timeout = 30 } = params;
519
348
 
520
- // Extract domain for policy matching
521
349
  let domain: string;
522
- try {
523
- domain = new URL(url).hostname;
524
- } catch {
525
- return {
526
- content: [{ type: "text", text: `Invalid URL: ${url}` }],
527
- isError: true,
528
- };
350
+ try { domain = new URL(url).hostname; } catch {
351
+ return { content: [{ type: "text", text: `Invalid URL: ${url}` }], isError: true };
529
352
  }
530
353
 
531
- // Authorize via Cedar
532
354
  const decision = await cedar.authorize({
533
355
  principal: `Agent::"openclaw"`,
534
356
  action: `Action::"call_api"`,
@@ -538,57 +360,29 @@ export default function register(api: OpenClawPluginApi) {
538
360
 
539
361
  if (decision.decision === "deny") {
540
362
  return {
541
- content: [
542
- {
543
- type: "text",
544
- text: `DENIED by Cedar policy: API call to "${domain}"\nURL: ${url}\nMethod: ${method}\nReason: ${decision.reasons.join(", ") || "default deny"}`,
545
- },
546
- ],
363
+ content: [{ type: "text", text: `DENIED by Cedar policy: API call to "${domain}"\nURL: ${url}\nReason: ${decision.reasons.join(", ") || "default deny"}` }],
547
364
  isError: true,
548
365
  };
549
366
  }
550
367
 
551
- // Make the HTTP request
552
368
  try {
553
369
  const controller = new AbortController();
554
370
  const timer = setTimeout(() => controller.abort(), timeout * 1000);
555
-
556
- const response = await fetch(url, {
557
- method,
558
- headers,
559
- body: body ?? undefined,
560
- signal: controller.signal,
561
- });
562
-
371
+ const response = await fetch(url, { method, headers, body: body ?? undefined, signal: controller.signal });
563
372
  clearTimeout(timer);
564
-
565
373
  const responseText = await response.text();
566
- const truncated = responseText.length > 50000
567
- ? responseText.slice(0, 50000) + "\n...[truncated]"
568
- : responseText;
569
-
570
- return {
571
- content: [
572
- {
573
- type: "text",
574
- text: `HTTP ${response.status} ${response.statusText}\n\n${truncated}`,
575
- },
576
- ],
577
- isError: !response.ok,
578
- };
374
+ const truncated = responseText.length > 50000 ? responseText.slice(0, 50000) + "\n...[truncated]" : responseText;
375
+ return { content: [{ type: "text", text: `HTTP ${response.status} ${response.statusText}\n\n${truncated}` }], isError: !response.ok };
579
376
  } catch (err: any) {
580
- return {
581
- content: [{ type: "text", text: `API call failed: ${err.message}` }],
582
- isError: true,
583
- };
377
+ return { content: [{ type: "text", text: `API call failed: ${err.message}` }], isError: true };
584
378
  }
585
379
  },
586
380
  }, { optional: true });
587
381
 
588
- // --- CLI command ---
382
+ // --- CLI commands ---
589
383
  api.registerCli?.(
590
384
  ({ program }) => {
591
- const cmd = program.command("carapace").description("Carapace — MCP tool authorization");
385
+ const cmd = program.command("carapace").description("Carapace — Cedar policy enforcement for agent tools");
592
386
 
593
387
  cmd.command("status").action(async () => {
594
388
  const servers = aggregator.getServerStatus();
@@ -602,13 +396,11 @@ export default function register(api: OpenClawPluginApi) {
602
396
  console.log(`\n ${enabled}/${tools.length} tools enabled`);
603
397
  console.log(` GUI: http://localhost:${config.guiPort ?? 19820}`);
604
398
 
605
- if (proxy) {
606
- const stats = proxy.getStats();
607
- console.log(`\n 🛡️ LLM Proxy: http://127.0.0.1:${proxyConfig!.port ?? 19821}`);
608
- console.log(` Requests: ${stats.requests} | Tool calls evaluated: ${stats.toolCallsEvaluated} | Denied: ${stats.toolCallsDenied}`);
609
- } else {
610
- console.log(`\n ⚠️ LLM Proxy: disabled`);
611
- }
399
+ const policies = cedar.getPolicies();
400
+ console.log(`\n 🛡️ Enforcement: before_tool_call hook`);
401
+ console.log(` Policies: ${policies.length} loaded`);
402
+ console.log(` Default: ${config.defaultPolicy ?? "allow-all"}`);
403
+ console.log(` Evaluated: ${stats.toolCallsEvaluated} | Denied: ${stats.toolCallsDenied}`);
612
404
  console.log();
613
405
  });
614
406
 
@@ -626,282 +418,102 @@ export default function register(api: OpenClawPluginApi) {
626
418
  console.log("✅ All policies verified");
627
419
  } else {
628
420
  console.log("⚠️ Verification issues:");
629
- for (const issue of result.issues) {
630
- console.log(` - ${issue}`);
631
- }
421
+ for (const issue of result.issues) console.log(` - ${issue}`);
632
422
  }
633
423
  });
634
424
 
635
425
  cmd.command("setup")
636
- .description("Configure OpenClaw to route all traffic through Carapace")
637
- .option("--no-proxy", "Skip LLM proxy setup (tool-deny mode only)")
638
- .action(async (opts: any) => {
639
- const { readFileSync, writeFileSync, existsSync, copyFileSync } = require("node:fs");
426
+ .description("Enable Carapace plugin in OpenClaw config")
427
+ .action(async () => {
428
+ const { readFileSync, writeFileSync, existsSync } = require("node:fs");
640
429
  const { join } = require("node:path");
641
430
  const { homedir } = require("node:os");
642
431
 
643
432
  console.log("\n🦞 Carapace Setup\n");
644
- backupConfig();
645
- console.log(" 📦 Backed up openclaw.json → openclaw.json.carapace-backup");
646
- let anyChanges = false;
647
433
 
648
434
  const configPath = join(homedir(), ".openclaw", "openclaw.json");
649
- const cfg = JSON.parse(readFileSync(configPath, "utf-8"));
650
- const skipProxy = opts.noProxy === true;
651
-
652
- // Detect if proxy is already configured in the live config
653
- const existingProxyEnabled = cfg.plugins?.entries?.carapace?.config?.proxy?.enabled;
654
-
655
- if (!skipProxy && !existingProxyEnabled) {
656
- // Enable the proxy by default — find the API key automatically
657
- console.log(" 🔍 Looking for Anthropic API key...");
658
- let apiKey = "";
659
-
660
- // Check auth.json
661
- const authPath = join(homedir(), ".openclaw", "agents", "main", "agent", "auth.json");
662
- if (existsSync(authPath)) {
663
- try {
664
- const auth = JSON.parse(readFileSync(authPath, "utf-8"));
665
- apiKey = auth.anthropic?.key ?? "";
666
- } catch {}
667
- }
668
-
669
- // Check environment
670
- if (!apiKey) apiKey = process.env.ANTHROPIC_API_KEY ?? "";
671
-
672
- if (!apiKey) {
673
- console.log(" ⚠️ Could not find Anthropic API key.");
674
- console.log(" Checked: ~/.openclaw/agents/main/agent/auth.json, ANTHROPIC_API_KEY env");
675
- console.log(" Falling back to tool-deny mode (no proxy).\n");
676
- } else {
677
- console.log(` Found key: ${apiKey.slice(0, 12)}...${apiKey.slice(-4)}`);
678
-
679
- // Write proxy config into plugin entry
680
- if (!cfg.plugins) cfg.plugins = {};
681
- if (!cfg.plugins.entries) cfg.plugins.entries = {};
682
- if (!cfg.plugins.entries.carapace) cfg.plugins.entries.carapace = {};
683
- cfg.plugins.entries.carapace.enabled = true;
684
- cfg.plugins.entries.carapace.config = {
685
- defaultPolicy: "allow-all",
686
- proxy: {
687
- enabled: true,
688
- port: 19821,
689
- upstream: "https://api.anthropic.com",
690
- apiKey,
691
- },
692
- };
693
-
694
- // Set models.providers.anthropic.baseUrl
695
- const proxyUrl = "http://127.0.0.1:19821";
696
- if (!cfg.models) cfg.models = {};
697
- if (!cfg.models.mode) cfg.models.mode = "merge";
698
- if (!cfg.models.providers) cfg.models.providers = {};
699
- if (!cfg.models.providers.anthropic) cfg.models.providers.anthropic = {};
700
- if (!Array.isArray(cfg.models.providers.anthropic.models)) {
701
- cfg.models.providers.anthropic.models = [];
702
- }
703
- if (cfg.models.providers.anthropic.baseUrl && cfg.models.providers.anthropic.baseUrl !== proxyUrl) {
704
- cfg.models.providers.anthropic._originalBaseUrl = cfg.models.providers.anthropic.baseUrl;
705
- }
706
- cfg.models.providers.anthropic.baseUrl = proxyUrl;
707
-
708
- // Do NOT deny built-in tools when proxy is enabled — proxy handles filtering
709
- // Remove any previous tool denials from earlier setup runs
710
- if (cfg.tools?.deny) {
711
- cfg.tools.deny = cfg.tools.deny.filter((t: string) => !BYPASS_TOOLS.includes(t));
712
- if (cfg.tools.deny.length === 0) delete cfg.tools.deny;
713
- if (cfg.tools && Object.keys(cfg.tools).length === 0) delete cfg.tools;
714
- }
715
-
716
- writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
717
-
718
- // Also patch models.json directly
719
- const modelsPath = join(homedir(), ".openclaw", "agents", "main", "agent", "models.json");
720
- if (existsSync(modelsPath)) {
721
- try {
722
- const modelsBackup = modelsPath + ".carapace-backup";
723
- if (!existsSync(modelsBackup)) copyFileSync(modelsPath, modelsBackup);
724
- const models = JSON.parse(readFileSync(modelsPath, "utf-8"));
725
- if (!models.providers) models.providers = {};
726
- if (!models.providers.anthropic) models.providers.anthropic = {};
727
- if (models.providers.anthropic.baseUrl && models.providers.anthropic.baseUrl !== proxyUrl) {
728
- models.providers.anthropic._originalBaseUrl = models.providers.anthropic.baseUrl;
729
- }
730
- models.providers.anthropic.baseUrl = proxyUrl;
731
- writeFileSync(modelsPath, JSON.stringify(models, null, 2) + "\n", "utf-8");
732
- console.log(" ✅ Patched models.json with proxy baseUrl");
733
- } catch (e: any) {
734
- console.log(` ⚠️ Could not patch models.json: ${e.message}`);
735
- }
736
- }
737
-
738
- console.log(" ✅ LLM proxy enabled (allow-all policy, port 19821)");
739
- console.log(" All API calls will route through Cedar.");
740
- console.log(" Built-in tools (exec, web_fetch, web_search) are NOT denied — proxy handles them.\n");
741
- anyChanges = true;
742
- }
743
- } else if (existingProxyEnabled) {
744
- console.log(" ✅ LLM proxy already configured.");
745
- // Ensure baseUrl is set
746
- console.log("\n Verifying baseUrl configuration:");
747
- const { patched, alreadySet } = patchConfigProxyBaseUrl();
748
- if (alreadySet.length > 0) console.log(` Already set: ${alreadySet.join(", ")}`);
749
- if (patched.length > 0) {
750
- console.log(` ✅ Set models.providers baseUrl for: ${patched.join(", ")}`);
751
- anyChanges = true;
752
- }
435
+ let cfg: any = {};
436
+ if (existsSync(configPath)) {
437
+ cfg = JSON.parse(readFileSync(configPath, "utf-8"));
753
438
  }
754
439
 
755
- // If proxy not enabled (skipped or no key), fall back to tool-deny mode
756
- const finalCfg = JSON.parse(readFileSync(configPath, "utf-8"));
757
- if (!finalCfg.plugins?.entries?.carapace?.config?.proxy?.enabled) {
758
- console.log(" Falling back to tool-deny mode:");
759
- const bypasses = checkForBypasses();
760
- if (bypasses.length > 0) {
761
- const { patched, alreadyDenied } = patchConfigDenyTools();
762
- if (alreadyDenied.length > 0) console.log(` Already denied: ${alreadyDenied.join(", ")}`);
763
- if (patched.length > 0) {
764
- console.log(` ✅ Added to tools.deny: ${patched.join(", ")}`);
765
- anyChanges = true;
766
- }
767
- } else {
768
- console.log(" ✅ Built-in bypass tools already denied.");
769
- }
440
+ if (!cfg.plugins) cfg.plugins = {};
441
+ if (!cfg.plugins.entries) cfg.plugins.entries = {};
442
+ if (!cfg.plugins.entries.carapace) cfg.plugins.entries.carapace = {};
443
+
444
+ const alreadyEnabled = cfg.plugins.entries.carapace.enabled === true;
445
+
446
+ cfg.plugins.entries.carapace.enabled = true;
447
+ if (!cfg.plugins.entries.carapace.config) {
448
+ cfg.plugins.entries.carapace.config = {
449
+ defaultPolicy: "allow-all",
450
+ };
770
451
  }
771
452
 
772
- if (anyChanges) {
453
+ writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
454
+
455
+ if (alreadyEnabled) {
456
+ console.log(" ✅ Carapace already enabled. No changes needed.\n");
457
+ } else {
458
+ console.log(" ✅ Enabled carapace plugin in openclaw.json");
459
+ console.log(" 🛡️ Cedar policies enforced via before_tool_call hook");
460
+ console.log(" 📋 No models.json or baseUrl changes needed");
773
461
  console.log("\n Restart the gateway for changes to take effect:");
774
462
  console.log(" openclaw gateway restart\n");
775
- } else {
776
- console.log("\n ✅ Everything already configured. No changes needed.\n");
777
463
  }
778
464
  });
779
465
 
780
466
  cmd.command("uninstall")
781
- .description("Reverse all config changes made by Carapace (restores built-in tools)")
467
+ .description("Disable Carapace plugin")
782
468
  .action(async () => {
783
- console.log("\n🦞 Carapace Uninstall\n");
784
- console.log(" This reverses changes made by 'openclaw carapace setup'.\n");
785
-
786
- try {
787
- const { readFileSync, writeFileSync, existsSync } = require("node:fs");
788
- const { join } = require("node:path");
789
- const { homedir } = require("node:os");
790
- const configPath = join(homedir(), ".openclaw", "openclaw.json");
791
-
792
- if (!existsSync(configPath)) {
793
- console.log(" No config file found. Nothing to undo.\n");
794
- return;
795
- }
796
-
797
- const cfg = JSON.parse(readFileSync(configPath, "utf-8"));
798
- let changed = false;
799
-
800
- // Remove Carapace-added entries from tools.deny
801
- if (cfg.tools?.deny) {
802
- const before = cfg.tools.deny.length;
803
- cfg.tools.deny = cfg.tools.deny.filter((t: string) => !BYPASS_TOOLS.includes(t));
804
- if (cfg.tools.deny.length === 0) delete cfg.tools.deny;
805
- if (cfg.tools && Object.keys(cfg.tools).length === 0) delete cfg.tools;
806
- if (cfg.tools?.deny?.length !== before) {
807
- changed = true;
808
- console.log(` ✅ Removed [${BYPASS_TOOLS.join(", ")}] from tools.deny`);
809
- console.log(" Built-in exec, web_fetch, and web_search are restored.");
810
- }
811
- }
812
-
813
- // Remove models.providers baseUrl override if it points at the proxy
814
- const proxyPort = cfg.plugins?.entries?.carapace?.config?.proxy?.port ?? 19821;
815
- const proxyUrl = `http://127.0.0.1:${proxyPort}`;
816
- if (cfg.models?.providers) {
817
- for (const [name, provCfg] of Object.entries(cfg.models.providers)) {
818
- if ((provCfg as any)?.baseUrl === proxyUrl) {
819
- // Restore original baseUrl if stored
820
- if ((provCfg as any)._originalBaseUrl) {
821
- (provCfg as any).baseUrl = (provCfg as any)._originalBaseUrl;
822
- delete (provCfg as any)._originalBaseUrl;
823
- console.log(` ✅ Restored original baseUrl for ${name}`);
824
- } else {
825
- delete (provCfg as any).baseUrl;
826
- console.log(` ✅ Removed baseUrl proxy override for ${name}`);
827
- }
828
- // Clean up empty objects
829
- if (Object.keys(provCfg as any).length === 0) delete cfg.models.providers[name];
830
- changed = true;
831
- console.log(` ${name} will connect directly to its API again.`);
832
- }
833
- }
834
- if (Object.keys(cfg.models.providers).length === 0) delete cfg.models.providers;
835
- if (cfg.models && Object.keys(cfg.models).length === 0) delete cfg.models;
836
- }
469
+ const { readFileSync, writeFileSync, existsSync } = require("node:fs");
470
+ const { join } = require("node:path");
471
+ const { homedir } = require("node:os");
837
472
 
838
- // Also clean up the per-agent models.json
839
- const agentModelsPath = join(homedir(), ".openclaw", "agents", "main", "agent", "models.json");
840
- if (existsSync(agentModelsPath)) {
841
- try {
842
- const agentModels = JSON.parse(readFileSync(agentModelsPath, "utf-8"));
843
- let modelsChanged = false;
844
- if (agentModels.providers) {
845
- for (const [name, provCfg] of Object.entries(agentModels.providers)) {
846
- if ((provCfg as any)?.baseUrl === proxyUrl) {
847
- if ((provCfg as any)._originalBaseUrl) {
848
- (provCfg as any).baseUrl = (provCfg as any)._originalBaseUrl;
849
- delete (provCfg as any)._originalBaseUrl;
850
- } else {
851
- delete (provCfg as any).baseUrl;
852
- }
853
- if (Object.keys(provCfg as any).length === 0) delete agentModels.providers[name];
854
- modelsChanged = true;
855
- console.log(` ✅ Cleaned proxy baseUrl from models.json for ${name}`);
856
- }
857
- }
858
- }
859
- if (modelsChanged) {
860
- writeFileSync(agentModelsPath, JSON.stringify(agentModels, null, 2) + "\n", "utf-8");
861
- changed = true;
862
- }
863
- } catch (e: any) {
864
- console.log(` ⚠️ Could not clean models.json: ${e.message}`);
865
- }
866
- }
473
+ console.log("\n🦞 Carapace Uninstall\n");
867
474
 
868
- // Disable the plugin entry (don't delete — user might want to re-enable)
869
- if (cfg.plugins?.entries?.carapace?.enabled) {
870
- cfg.plugins.entries.carapace.enabled = false;
871
- changed = true;
872
- console.log(" ✅ Disabled carapace plugin in config");
873
- }
475
+ const configPath = join(homedir(), ".openclaw", "openclaw.json");
476
+ if (!existsSync(configPath)) {
477
+ console.log(" No config file found. Nothing to undo.\n");
478
+ return;
479
+ }
874
480
 
875
- if (changed) {
876
- writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
877
- console.log("\n Config updated. Restart the gateway for changes to take effect:");
878
- console.log(" openclaw gateway restart\n");
879
- console.log(" To fully remove the plugin files:");
880
- console.log(" rm -rf ~/.openclaw/extensions/carapace\n");
881
- } else {
882
- console.log(" No Carapace changes found in config. Nothing to undo.\n");
883
- }
481
+ const cfg = JSON.parse(readFileSync(configPath, "utf-8"));
482
+ let changed = false;
884
483
 
484
+ if (cfg.plugins?.entries?.carapace?.enabled) {
485
+ cfg.plugins.entries.carapace.enabled = false;
486
+ changed = true;
487
+ console.log(" ✅ Disabled carapace plugin");
488
+ }
885
489
 
886
- } catch (err: any) {
887
- console.log(` ❌ Error: ${err.message}\n`);
490
+ if (changed) {
491
+ writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
492
+ console.log("\n Restart the gateway for changes to take effect:");
493
+ console.log(" openclaw gateway restart\n");
494
+ console.log(" To fully remove the plugin files:");
495
+ console.log(" rm -rf ~/.openclaw/extensions/carapace\n");
496
+ } else {
497
+ console.log(" Carapace already disabled. Nothing to undo.\n");
888
498
  }
889
499
  });
890
500
 
891
501
  cmd.command("check")
892
- .description("Check for bypass vulnerabilities (built-in tools that skip Cedar)")
502
+ .description("Check Cedar policy status")
893
503
  .action(async () => {
894
504
  console.log("\n🦞 Carapace Security Check\n");
895
- const bypasses = checkForBypasses();
896
- if (bypasses.length === 0) {
897
- console.log(" No bypass vulnerabilities found.");
898
- console.log(" All agent exec/fetch operations go through Cedar.\n");
505
+ const policies = cedar.getPolicies();
506
+ if (policies.length === 0) {
507
+ console.log(" ⚠️ No Cedar policies loaded.");
508
+ console.log(` Add policies to ${config.policyDir ?? "~/.openclaw/mcp-policies/"}\n`);
899
509
  } else {
900
- console.log(" ⚠️ Bypass vulnerabilities found:\n");
901
- for (const tool of bypasses) {
902
- console.log(` 🔓 ${tool} agents can bypass Cedar policies via this tool`);
510
+ console.log(` ${policies.length} Cedar policies loaded`);
511
+ console.log(` 🛡️ Enforcement via before_tool_call hook`);
512
+ console.log(` Default: ${config.defaultPolicy ?? "allow-all"}\n`);
513
+ for (const p of policies) {
514
+ console.log(` ${p.effect === "permit" ? "🟢" : "🔴"} ${p.id}`);
903
515
  }
904
- console.log(`\n Run "openclaw carapace setup" to fix.\n`);
516
+ console.log();
905
517
  }
906
518
  });
907
519
  },
@@ -912,6 +524,14 @@ export default function register(api: OpenClawPluginApi) {
912
524
  api.registerGatewayMethod?.("carapace.status", ({ respond }) => {
913
525
  const servers = aggregator.getServerStatus();
914
526
  const tools = aggregator.listTools();
915
- respond(true, { servers, toolCount: tools.length, enabledCount: tools.filter((t) => t.enabled).length });
527
+ const policies = cedar.getPolicies();
528
+ respond(true, {
529
+ servers,
530
+ toolCount: tools.length,
531
+ enabledCount: tools.filter((t) => t.enabled).length,
532
+ policyCount: policies.length,
533
+ enforcement: "before_tool_call",
534
+ stats,
535
+ });
916
536
  });
917
537
  }