@clawdreyhepburn/carapace 0.5.0 → 1.0.0
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/openclaw.plugin.json +13 -82
- package/package.json +2 -2
- package/src/index.ts +206 -586
package/openclaw.plugin.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "carapace",
|
|
3
3
|
"name": "Carapace",
|
|
4
|
-
"description": "
|
|
5
|
-
"version": "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
|
-
|
|
32
|
-
},
|
|
33
|
-
"
|
|
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
|
|
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
|
-
|
|
100
|
-
|
|
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.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0",
|
|
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
|
-
*
|
|
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
|
|
|
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, handler: (event: any) => Promise<any> | any): 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
|
-
*
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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:
|
|
98
|
+
proxyEnabled: false, // proxy no longer used
|
|
110
99
|
});
|
|
111
100
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
196
|
-
|
|
150
|
+
const decision = await cedar.authorize({
|
|
151
|
+
principal: `Agent::"openclaw"`,
|
|
152
|
+
action: `Action::"${action}"`,
|
|
153
|
+
resource: `${resourceType}::"${resourceId}"`,
|
|
154
|
+
context,
|
|
155
|
+
});
|
|
197
156
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
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
|
|
280
|
-
|
|
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
|
|
320
|
-
`
|
|
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
|
|
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:
|
|
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.
|
|
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
|
-
|
|
422
|
-
|
|
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 ?? "?"}): ${
|
|
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:
|
|
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
|
|
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
|
-
|
|
495
|
-
|
|
496
|
-
},
|
|
497
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
382
|
+
// --- CLI commands ---
|
|
589
383
|
api.registerCli?.(
|
|
590
384
|
({ program }) => {
|
|
591
|
-
const cmd = program.command("carapace").description("Carapace —
|
|
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
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
}
|
|
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("
|
|
637
|
-
.
|
|
638
|
-
|
|
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
|
-
|
|
650
|
-
|
|
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
|
-
|
|
756
|
-
|
|
757
|
-
if (!
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|
-
|
|
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("
|
|
467
|
+
.description("Disable Carapace plugin")
|
|
782
468
|
.action(async () => {
|
|
783
|
-
|
|
784
|
-
|
|
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
|
-
|
|
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
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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
|
-
|
|
876
|
-
|
|
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
|
-
|
|
887
|
-
|
|
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
|
|
502
|
+
.description("Check Cedar policy status")
|
|
893
503
|
.action(async () => {
|
|
894
504
|
console.log("\n🦞 Carapace Security Check\n");
|
|
895
|
-
const
|
|
896
|
-
if (
|
|
897
|
-
console.log("
|
|
898
|
-
console.log(
|
|
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(
|
|
901
|
-
|
|
902
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
}
|