@clawdreyhepburn/carapace 0.2.0 ā 0.3.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/README.md +80 -18
- package/docs/RECOMMENDED-POLICIES.md +519 -0
- package/package.json +1 -1
- package/src/cedar-engine-cedarling.ts +15 -0
- package/src/index.ts +147 -1
- package/src/llm-proxy.ts +648 -0
- package/src/types.ts +9 -0
package/src/index.ts
CHANGED
|
@@ -8,6 +8,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";
|
|
11
12
|
import type { PluginConfig } from "./types.js";
|
|
12
13
|
|
|
13
14
|
export const id = "carapace";
|
|
@@ -64,6 +65,71 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
64
65
|
logger,
|
|
65
66
|
});
|
|
66
67
|
|
|
68
|
+
// --- LLM Proxy: intercept tool calls at the API level ---
|
|
69
|
+
const proxyConfig = config.proxy;
|
|
70
|
+
const proxy = proxyConfig?.enabled ? new LlmProxy({
|
|
71
|
+
port: proxyConfig.port ?? 19821,
|
|
72
|
+
upstream: {
|
|
73
|
+
anthropic: proxyConfig.upstream?.anthropic ? {
|
|
74
|
+
url: proxyConfig.upstream.anthropic.url ?? "https://api.anthropic.com",
|
|
75
|
+
apiKey: proxyConfig.upstream.anthropic.apiKey,
|
|
76
|
+
} : undefined,
|
|
77
|
+
openai: proxyConfig.upstream?.openai ? {
|
|
78
|
+
url: proxyConfig.upstream.openai.url ?? "https://api.openai.com",
|
|
79
|
+
apiKey: proxyConfig.upstream.openai.apiKey,
|
|
80
|
+
} : undefined,
|
|
81
|
+
},
|
|
82
|
+
cedar,
|
|
83
|
+
logger,
|
|
84
|
+
}) : null;
|
|
85
|
+
|
|
86
|
+
// --- Bypass detection: warn if built-in tools aren't denied ---
|
|
87
|
+
const BYPASS_TOOLS = ["exec", "web_fetch", "web_search"];
|
|
88
|
+
|
|
89
|
+
function checkForBypasses(): string[] {
|
|
90
|
+
// Read OpenClaw config to check tools.deny
|
|
91
|
+
try {
|
|
92
|
+
const { readFileSync, existsSync } = require("node:fs");
|
|
93
|
+
const { join } = require("node:path");
|
|
94
|
+
const { homedir } = require("node:os");
|
|
95
|
+
const configPath = join(homedir(), ".openclaw", "openclaw.json");
|
|
96
|
+
if (!existsSync(configPath)) return BYPASS_TOOLS;
|
|
97
|
+
const cfg = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
98
|
+
const denied: string[] = cfg.tools?.deny ?? [];
|
|
99
|
+
return BYPASS_TOOLS.filter((t) => !denied.includes(t));
|
|
100
|
+
} catch {
|
|
101
|
+
return BYPASS_TOOLS;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function patchConfigDenyTools(): { patched: string[]; alreadyDenied: string[] } {
|
|
106
|
+
const { readFileSync, writeFileSync, existsSync } = require("node:fs");
|
|
107
|
+
const { join } = require("node:path");
|
|
108
|
+
const { homedir } = require("node:os");
|
|
109
|
+
const configPath = join(homedir(), ".openclaw", "openclaw.json");
|
|
110
|
+
|
|
111
|
+
let cfg: any = {};
|
|
112
|
+
if (existsSync(configPath)) {
|
|
113
|
+
cfg = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!cfg.tools) cfg.tools = {};
|
|
117
|
+
if (!cfg.tools.deny) cfg.tools.deny = [];
|
|
118
|
+
|
|
119
|
+
const alreadyDenied = BYPASS_TOOLS.filter((t) => cfg.tools.deny.includes(t));
|
|
120
|
+
const toAdd = BYPASS_TOOLS.filter((t) => !cfg.tools.deny.includes(t));
|
|
121
|
+
|
|
122
|
+
for (const tool of toAdd) {
|
|
123
|
+
cfg.tools.deny.push(tool);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (toAdd.length > 0) {
|
|
127
|
+
writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { patched: toAdd, alreadyDenied };
|
|
131
|
+
}
|
|
132
|
+
|
|
67
133
|
// --- Background service: connect to MCP servers and serve GUI ---
|
|
68
134
|
api.registerService({
|
|
69
135
|
id: "carapace",
|
|
@@ -73,8 +139,27 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
73
139
|
await aggregator.connectAll();
|
|
74
140
|
await gui.start();
|
|
75
141
|
logger.info(`Control GUI at http://localhost:${config.guiPort ?? 19820}`);
|
|
142
|
+
|
|
143
|
+
if (proxy) {
|
|
144
|
+
await proxy.start();
|
|
145
|
+
logger.info(
|
|
146
|
+
`š”ļø LLM Proxy active on http://127.0.0.1:${proxyConfig!.port ?? 19821} ā ` +
|
|
147
|
+
`all tool calls go through Cedar`
|
|
148
|
+
);
|
|
149
|
+
} else {
|
|
150
|
+
// Check for bypass vulnerabilities only when proxy is disabled
|
|
151
|
+
const bypasses = checkForBypasses();
|
|
152
|
+
if (bypasses.length > 0) {
|
|
153
|
+
logger.warn(
|
|
154
|
+
`ā ļø BYPASS RISK: Built-in tools [${bypasses.join(", ")}] are NOT denied and LLM proxy is not enabled. ` +
|
|
155
|
+
`Agents can use these to bypass Carapace Cedar policies. ` +
|
|
156
|
+
`Enable the LLM proxy (recommended) or run "openclaw carapace setup" to deny built-in tools.`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
76
160
|
},
|
|
77
161
|
async stop() {
|
|
162
|
+
if (proxy) await proxy.stop();
|
|
78
163
|
await gui.stop();
|
|
79
164
|
await aggregator.disconnectAll();
|
|
80
165
|
logger.info("Carapace stopped");
|
|
@@ -351,7 +436,16 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
351
436
|
const tools = aggregator.listTools();
|
|
352
437
|
const enabled = tools.filter((t) => t.enabled).length;
|
|
353
438
|
console.log(`\n ${enabled}/${tools.length} tools enabled`);
|
|
354
|
-
console.log(` GUI: http://localhost:${config.guiPort ?? 19820}
|
|
439
|
+
console.log(` GUI: http://localhost:${config.guiPort ?? 19820}`);
|
|
440
|
+
|
|
441
|
+
if (proxy) {
|
|
442
|
+
const stats = proxy.getStats();
|
|
443
|
+
console.log(`\n š”ļø LLM Proxy: http://127.0.0.1:${proxyConfig!.port ?? 19821}`);
|
|
444
|
+
console.log(` Requests: ${stats.requests} | Tool calls evaluated: ${stats.toolCallsEvaluated} | Denied: ${stats.toolCallsDenied}`);
|
|
445
|
+
} else {
|
|
446
|
+
console.log(`\n ā ļø LLM Proxy: disabled`);
|
|
447
|
+
}
|
|
448
|
+
console.log();
|
|
355
449
|
});
|
|
356
450
|
|
|
357
451
|
cmd.command("tools").action(async () => {
|
|
@@ -373,6 +467,58 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
373
467
|
}
|
|
374
468
|
}
|
|
375
469
|
});
|
|
470
|
+
|
|
471
|
+
cmd.command("setup")
|
|
472
|
+
.description("Configure OpenClaw to route exec/fetch through Carapace (denies built-in bypass tools)")
|
|
473
|
+
.action(async () => {
|
|
474
|
+
console.log("\nš¦ Carapace Setup\n");
|
|
475
|
+
|
|
476
|
+
const bypasses = checkForBypasses();
|
|
477
|
+
if (bypasses.length === 0) {
|
|
478
|
+
console.log(" ā
All bypass tools are already denied. No changes needed.\n");
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
console.log(" Built-in tools that bypass Carapace Cedar policies:");
|
|
483
|
+
for (const tool of bypasses) {
|
|
484
|
+
console.log(` ā ļø ${tool} ā agents can use this to skip Cedar authorization`);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
console.log("\n This will add the following to your OpenClaw config:");
|
|
488
|
+
console.log(` tools.deny: [${bypasses.map(t => `"${t}"`).join(", ")}]`);
|
|
489
|
+
console.log("\n After setup, agents must use carapace_exec and carapace_fetch");
|
|
490
|
+
console.log(" instead, which enforce Cedar policies on every call.\n");
|
|
491
|
+
|
|
492
|
+
const { patched, alreadyDenied } = patchConfigDenyTools();
|
|
493
|
+
|
|
494
|
+
if (alreadyDenied.length > 0) {
|
|
495
|
+
console.log(` Already denied: ${alreadyDenied.join(", ")}`);
|
|
496
|
+
}
|
|
497
|
+
if (patched.length > 0) {
|
|
498
|
+
console.log(` ā
Added to tools.deny: ${patched.join(", ")}`);
|
|
499
|
+
console.log("\n Restart the gateway for changes to take effect:");
|
|
500
|
+
console.log(" openclaw gateway restart\n");
|
|
501
|
+
} else {
|
|
502
|
+
console.log(" ā
No changes needed.\n");
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
cmd.command("check")
|
|
507
|
+
.description("Check for bypass vulnerabilities (built-in tools that skip Cedar)")
|
|
508
|
+
.action(async () => {
|
|
509
|
+
console.log("\nš¦ Carapace Security Check\n");
|
|
510
|
+
const bypasses = checkForBypasses();
|
|
511
|
+
if (bypasses.length === 0) {
|
|
512
|
+
console.log(" ā
No bypass vulnerabilities found.");
|
|
513
|
+
console.log(" All agent exec/fetch operations go through Cedar.\n");
|
|
514
|
+
} else {
|
|
515
|
+
console.log(" ā ļø Bypass vulnerabilities found:\n");
|
|
516
|
+
for (const tool of bypasses) {
|
|
517
|
+
console.log(` š ${tool} ā agents can bypass Cedar policies via this tool`);
|
|
518
|
+
}
|
|
519
|
+
console.log(`\n Run "openclaw carapace setup" to fix.\n`);
|
|
520
|
+
}
|
|
521
|
+
});
|
|
376
522
|
},
|
|
377
523
|
{ commands: ["carapace"] },
|
|
378
524
|
);
|