@clawdreyhepburn/carapace 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +337 -280
- package/docs/RECOMMENDED-POLICIES.md +189 -378
- package/docs/SECURITY.md +544 -0
- package/openclaw.plugin.json +31 -2
- package/package.json +1 -1
- package/src/index.ts +194 -28
- 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,24 @@ 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
|
+
|
|
67
86
|
// --- Bypass detection: warn if built-in tools aren't denied ---
|
|
68
87
|
const BYPASS_TOOLS = ["exec", "web_fetch", "web_search"];
|
|
69
88
|
|
|
@@ -111,6 +130,46 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
111
130
|
return { patched: toAdd, alreadyDenied };
|
|
112
131
|
}
|
|
113
132
|
|
|
133
|
+
function patchConfigProxyBaseUrl(): { patched: string[]; alreadySet: string[] } {
|
|
134
|
+
const { readFileSync, writeFileSync, existsSync } = require("node:fs");
|
|
135
|
+
const { join } = require("node:path");
|
|
136
|
+
const { homedir } = require("node:os");
|
|
137
|
+
const configPath = join(homedir(), ".openclaw", "openclaw.json");
|
|
138
|
+
|
|
139
|
+
if (!existsSync(configPath)) return { patched: [], alreadySet: [] };
|
|
140
|
+
const cfg = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
141
|
+
|
|
142
|
+
const port = config.proxy?.port ?? 19821;
|
|
143
|
+
const proxyUrl = `http://127.0.0.1:${port}`;
|
|
144
|
+
|
|
145
|
+
// Figure out which providers have upstream keys configured
|
|
146
|
+
const providers: string[] = [];
|
|
147
|
+
if (config.proxy?.upstream?.anthropic) providers.push("anthropic");
|
|
148
|
+
if (config.proxy?.upstream?.openai) providers.push("openai");
|
|
149
|
+
|
|
150
|
+
const patched: string[] = [];
|
|
151
|
+
const alreadySet: string[] = [];
|
|
152
|
+
|
|
153
|
+
if (!cfg.models) cfg.models = {};
|
|
154
|
+
if (!cfg.models.providers) cfg.models.providers = {};
|
|
155
|
+
|
|
156
|
+
for (const provider of providers) {
|
|
157
|
+
if (!cfg.models.providers[provider]) cfg.models.providers[provider] = {};
|
|
158
|
+
if (cfg.models.providers[provider].baseUrl === proxyUrl) {
|
|
159
|
+
alreadySet.push(provider);
|
|
160
|
+
} else {
|
|
161
|
+
cfg.models.providers[provider].baseUrl = proxyUrl;
|
|
162
|
+
patched.push(provider);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (patched.length > 0) {
|
|
167
|
+
writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { patched, alreadySet };
|
|
171
|
+
}
|
|
172
|
+
|
|
114
173
|
// --- Background service: connect to MCP servers and serve GUI ---
|
|
115
174
|
api.registerService({
|
|
116
175
|
id: "carapace",
|
|
@@ -121,17 +180,26 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
121
180
|
await gui.start();
|
|
122
181
|
logger.info(`Control GUI at http://localhost:${config.guiPort ?? 19820}`);
|
|
123
182
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
`Agents can use these to bypass Carapace Cedar policies. ` +
|
|
130
|
-
`Run "openclaw carapace setup" to fix this automatically.`
|
|
183
|
+
if (proxy) {
|
|
184
|
+
await proxy.start();
|
|
185
|
+
logger.info(
|
|
186
|
+
`🛡️ LLM Proxy active on http://127.0.0.1:${proxyConfig!.port ?? 19821} — ` +
|
|
187
|
+
`all tool calls go through Cedar`
|
|
131
188
|
);
|
|
189
|
+
} else {
|
|
190
|
+
// Check for bypass vulnerabilities only when proxy is disabled
|
|
191
|
+
const bypasses = checkForBypasses();
|
|
192
|
+
if (bypasses.length > 0) {
|
|
193
|
+
logger.warn(
|
|
194
|
+
`⚠️ BYPASS RISK: Built-in tools [${bypasses.join(", ")}] are NOT denied and LLM proxy is not enabled. ` +
|
|
195
|
+
`Agents can use these to bypass Carapace Cedar policies. ` +
|
|
196
|
+
`Enable the LLM proxy (recommended) or run "openclaw carapace setup" to deny built-in tools.`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
132
199
|
}
|
|
133
200
|
},
|
|
134
201
|
async stop() {
|
|
202
|
+
if (proxy) await proxy.stop();
|
|
135
203
|
await gui.stop();
|
|
136
204
|
await aggregator.disconnectAll();
|
|
137
205
|
logger.info("Carapace stopped");
|
|
@@ -408,7 +476,16 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
408
476
|
const tools = aggregator.listTools();
|
|
409
477
|
const enabled = tools.filter((t) => t.enabled).length;
|
|
410
478
|
console.log(`\n ${enabled}/${tools.length} tools enabled`);
|
|
411
|
-
console.log(` GUI: http://localhost:${config.guiPort ?? 19820}
|
|
479
|
+
console.log(` GUI: http://localhost:${config.guiPort ?? 19820}`);
|
|
480
|
+
|
|
481
|
+
if (proxy) {
|
|
482
|
+
const stats = proxy.getStats();
|
|
483
|
+
console.log(`\n 🛡️ LLM Proxy: http://127.0.0.1:${proxyConfig!.port ?? 19821}`);
|
|
484
|
+
console.log(` Requests: ${stats.requests} | Tool calls evaluated: ${stats.toolCallsEvaluated} | Denied: ${stats.toolCallsDenied}`);
|
|
485
|
+
} else {
|
|
486
|
+
console.log(`\n ⚠️ LLM Proxy: disabled`);
|
|
487
|
+
}
|
|
488
|
+
console.log();
|
|
412
489
|
});
|
|
413
490
|
|
|
414
491
|
cmd.command("tools").action(async () => {
|
|
@@ -432,37 +509,126 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
432
509
|
});
|
|
433
510
|
|
|
434
511
|
cmd.command("setup")
|
|
435
|
-
.description("Configure OpenClaw to route
|
|
512
|
+
.description("Configure OpenClaw to route all traffic through Carapace")
|
|
436
513
|
.action(async () => {
|
|
437
514
|
console.log("\n🦞 Carapace Setup\n");
|
|
515
|
+
let anyChanges = false;
|
|
438
516
|
|
|
517
|
+
// 1. Deny built-in bypass tools
|
|
439
518
|
const bypasses = checkForBypasses();
|
|
440
|
-
if (bypasses.length
|
|
441
|
-
console.log("
|
|
442
|
-
|
|
519
|
+
if (bypasses.length > 0) {
|
|
520
|
+
console.log(" Denying built-in tools that bypass Cedar:");
|
|
521
|
+
const { patched, alreadyDenied } = patchConfigDenyTools();
|
|
522
|
+
if (alreadyDenied.length > 0) {
|
|
523
|
+
console.log(` Already denied: ${alreadyDenied.join(", ")}`);
|
|
524
|
+
}
|
|
525
|
+
if (patched.length > 0) {
|
|
526
|
+
console.log(` ✅ Added to tools.deny: ${patched.join(", ")}`);
|
|
527
|
+
anyChanges = true;
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
console.log(" ✅ Built-in bypass tools already denied.");
|
|
443
531
|
}
|
|
444
532
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
console.log(
|
|
533
|
+
// 2. Set up LLM proxy baseUrl if proxy is configured
|
|
534
|
+
if (config.proxy?.enabled) {
|
|
535
|
+
console.log("\n Configuring LLM proxy baseUrl:");
|
|
536
|
+
const { patched, alreadySet } = patchConfigProxyBaseUrl();
|
|
537
|
+
if (alreadySet.length > 0) {
|
|
538
|
+
console.log(` Already set: ${alreadySet.join(", ")}`);
|
|
539
|
+
}
|
|
540
|
+
if (patched.length > 0) {
|
|
541
|
+
console.log(` ✅ Set models.providers baseUrl for: ${patched.join(", ")}`);
|
|
542
|
+
anyChanges = true;
|
|
543
|
+
}
|
|
544
|
+
if (patched.length === 0 && alreadySet.length === 0) {
|
|
545
|
+
console.log(" ⚠️ No upstream providers configured in proxy config.");
|
|
546
|
+
console.log(" Add proxy.upstream.anthropic or proxy.upstream.openai to your plugin config.");
|
|
547
|
+
}
|
|
548
|
+
} else {
|
|
549
|
+
console.log("\n LLM proxy not enabled — skipping baseUrl setup.");
|
|
550
|
+
console.log(" To enable, add proxy.enabled: true to your Carapace plugin config.");
|
|
448
551
|
}
|
|
449
552
|
|
|
450
|
-
|
|
451
|
-
console.log(` tools.deny: [${bypasses.map(t => `"${t}"`).join(", ")}]`);
|
|
452
|
-
console.log("\n After setup, agents must use carapace_exec and carapace_fetch");
|
|
453
|
-
console.log(" instead, which enforce Cedar policies on every call.\n");
|
|
454
|
-
|
|
455
|
-
const { patched, alreadyDenied } = patchConfigDenyTools();
|
|
456
|
-
|
|
457
|
-
if (alreadyDenied.length > 0) {
|
|
458
|
-
console.log(` Already denied: ${alreadyDenied.join(", ")}`);
|
|
459
|
-
}
|
|
460
|
-
if (patched.length > 0) {
|
|
461
|
-
console.log(` ✅ Added to tools.deny: ${patched.join(", ")}`);
|
|
553
|
+
if (anyChanges) {
|
|
462
554
|
console.log("\n Restart the gateway for changes to take effect:");
|
|
463
555
|
console.log(" openclaw gateway restart\n");
|
|
464
556
|
} else {
|
|
465
|
-
console.log(" ✅ No changes needed.\n");
|
|
557
|
+
console.log("\n ✅ Everything already configured. No changes needed.\n");
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
cmd.command("uninstall")
|
|
562
|
+
.description("Reverse all config changes made by Carapace (restores built-in tools)")
|
|
563
|
+
.action(async () => {
|
|
564
|
+
console.log("\n🦞 Carapace Uninstall\n");
|
|
565
|
+
console.log(" This reverses changes made by 'openclaw carapace setup'.\n");
|
|
566
|
+
|
|
567
|
+
try {
|
|
568
|
+
const { readFileSync, writeFileSync, existsSync } = require("node:fs");
|
|
569
|
+
const { join } = require("node:path");
|
|
570
|
+
const { homedir } = require("node:os");
|
|
571
|
+
const configPath = join(homedir(), ".openclaw", "openclaw.json");
|
|
572
|
+
|
|
573
|
+
if (!existsSync(configPath)) {
|
|
574
|
+
console.log(" No config file found. Nothing to undo.\n");
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const cfg = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
579
|
+
let changed = false;
|
|
580
|
+
|
|
581
|
+
// Remove Carapace-added entries from tools.deny
|
|
582
|
+
if (cfg.tools?.deny) {
|
|
583
|
+
const before = cfg.tools.deny.length;
|
|
584
|
+
cfg.tools.deny = cfg.tools.deny.filter((t: string) => !BYPASS_TOOLS.includes(t));
|
|
585
|
+
if (cfg.tools.deny.length === 0) delete cfg.tools.deny;
|
|
586
|
+
if (cfg.tools && Object.keys(cfg.tools).length === 0) delete cfg.tools;
|
|
587
|
+
if (cfg.tools?.deny?.length !== before) {
|
|
588
|
+
changed = true;
|
|
589
|
+
console.log(` ✅ Removed [${BYPASS_TOOLS.join(", ")}] from tools.deny`);
|
|
590
|
+
console.log(" Built-in exec, web_fetch, and web_search are restored.");
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Remove models.providers baseUrl override if it points at the proxy
|
|
595
|
+
const proxyPort = cfg.plugins?.entries?.carapace?.config?.proxy?.port ?? 19821;
|
|
596
|
+
const proxyUrl = `http://127.0.0.1:${proxyPort}`;
|
|
597
|
+
if (cfg.models?.providers) {
|
|
598
|
+
for (const [name, provCfg] of Object.entries(cfg.models.providers)) {
|
|
599
|
+
if ((provCfg as any)?.baseUrl === proxyUrl) {
|
|
600
|
+
delete (provCfg as any).baseUrl;
|
|
601
|
+
// Clean up empty objects
|
|
602
|
+
if (Object.keys(provCfg as any).length === 0) delete cfg.models.providers[name];
|
|
603
|
+
changed = true;
|
|
604
|
+
console.log(` ✅ Removed baseUrl proxy override for ${name}`);
|
|
605
|
+
console.log(` ${name} will connect directly to its API again.`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (Object.keys(cfg.models.providers).length === 0) delete cfg.models.providers;
|
|
609
|
+
if (cfg.models && Object.keys(cfg.models).length === 0) delete cfg.models;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Disable the plugin entry (don't delete — user might want to re-enable)
|
|
613
|
+
if (cfg.plugins?.entries?.carapace?.enabled) {
|
|
614
|
+
cfg.plugins.entries.carapace.enabled = false;
|
|
615
|
+
changed = true;
|
|
616
|
+
console.log(" ✅ Disabled carapace plugin in config");
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (changed) {
|
|
620
|
+
writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
|
|
621
|
+
console.log("\n Config updated. Restart the gateway for changes to take effect:");
|
|
622
|
+
console.log(" openclaw gateway restart\n");
|
|
623
|
+
console.log(" To fully remove the plugin files:");
|
|
624
|
+
console.log(" rm -rf ~/.openclaw/extensions/carapace\n");
|
|
625
|
+
} else {
|
|
626
|
+
console.log(" No Carapace changes found in config. Nothing to undo.\n");
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
} catch (err: any) {
|
|
631
|
+
console.log(` ❌ Error: ${err.message}\n`);
|
|
466
632
|
}
|
|
467
633
|
});
|
|
468
634
|
|