@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/README.md +58 -166
- package/dist/cedar-engine-cedarling.js +19 -65
- package/dist/cedar-engine-cedarling.js.map +1 -1
- package/dist/gui/html.js +14 -0
- package/dist/gui/html.js.map +1 -1
- package/dist/gui/server.d.ts +2 -0
- package/dist/gui/server.js +6 -14
- package/dist/gui/server.js.map +1 -1
- package/dist/index.d.ts +20 -5
- package/dist/index.js +230 -337
- package/dist/index.js.map +1 -1
- package/dist/llm-proxy.d.ts +1 -2
- package/dist/llm-proxy.js +6 -10
- package/dist/llm-proxy.js.map +1 -1
- package/dist/policy-source.d.ts +13 -0
- package/dist/policy-source.js +23 -0
- package/dist/policy-source.js.map +1 -1
- package/dist/types.d.ts +4 -1
- package/docs/RECOMMENDED-POLICIES.md +2 -2
- package/docs/SECURITY.md +36 -136
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/policies/00-schema.cedar +30 -0
- package/policies/10-shell-deny.cedar +51 -0
- package/policies/20-shell-permit.cedar +60 -0
- package/policies/30-shell-path-guards.cedar +86 -0
- package/policies/40-api-policy.cedar +70 -0
- package/policies/50-tool-policy.cedar +38 -0
- package/policies/schema.json +88 -0
- package/src/index.ts +8 -3
- package/src/llm-proxy.ts +13 -0
- package/src/policy-source.ts +22 -0
package/dist/index.js
CHANGED
|
@@ -1,16 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Carapace — OpenClaw Plugin
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
// ---
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
123
|
-
|
|
110
|
+
// MCP or other tools
|
|
111
|
+
context = params ? { arguments: params } : {};
|
|
124
112
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
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:
|
|
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.
|
|
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
|
-
|
|
244
|
-
|
|
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 ?? "?"}): ${
|
|
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:
|
|
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
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
},
|
|
314
|
-
|
|
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
|
-
|
|
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
|
|
336
|
+
// --- CLI commands ---
|
|
397
337
|
api.registerCli?.(({ program }) => {
|
|
398
|
-
const cmd = program.command("carapace").description("Carapace —
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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("
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
458
|
-
|
|
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
|
-
|
|
461
|
-
if (
|
|
462
|
-
console.log("
|
|
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("
|
|
478
|
-
console.log("
|
|
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("
|
|
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
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
-
|
|
556
|
-
|
|
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
|
|
443
|
+
.description("Check Cedar policy status")
|
|
561
444
|
.action(async () => {
|
|
562
445
|
console.log("\n🦞 Carapace Security Check\n");
|
|
563
|
-
const
|
|
564
|
-
if (
|
|
565
|
-
console.log("
|
|
566
|
-
console.log(
|
|
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(
|
|
570
|
-
|
|
571
|
-
|
|
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(
|
|
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
|
-
|
|
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
|