@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/CHANGELOG.md +15 -0
- package/README.md +60 -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 +236 -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/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 +26 -14
- package/src/llm-proxy.ts +13 -0
- package/src/policy-source.ts +22 -0
package/dist/index.js
CHANGED
|
@@ -1,16 +1,58 @@
|
|
|
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
|
+
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
|
-
// ---
|
|
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);
|
|
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
|
-
|
|
123
|
-
|
|
122
|
+
// MCP or other tools
|
|
123
|
+
context = params ? { arguments: params } : {};
|
|
124
124
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
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
|
-
}
|
|
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
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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
|
-
},
|
|
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 ?? "?"}): ${
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
},
|
|
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
|
-
|
|
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
|
|
348
|
+
// --- CLI commands ---
|
|
397
349
|
api.registerCli?.(({ program }) => {
|
|
398
|
-
const cmd = program.command("carapace").description("Carapace —
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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("
|
|
388
|
+
.description("Enable Carapace plugin in OpenClaw config")
|
|
441
389
|
.action(async () => {
|
|
442
390
|
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
|
-
}
|
|
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
|
-
|
|
458
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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("
|
|
478
|
-
console.log("
|
|
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("
|
|
422
|
+
.description("Disable Carapace plugin")
|
|
490
423
|
.action(async () => {
|
|
491
424
|
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
|
-
}
|
|
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
|
-
|
|
556
|
-
|
|
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
|
|
449
|
+
.description("Check Cedar policy status")
|
|
561
450
|
.action(async () => {
|
|
562
451
|
console.log("\n🦞 Carapace Security Check\n");
|
|
563
|
-
const
|
|
564
|
-
if (
|
|
565
|
-
console.log("
|
|
566
|
-
console.log(
|
|
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(
|
|
570
|
-
|
|
571
|
-
|
|
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(
|
|
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
|
-
|
|
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
|