@clawdreyhepburn/carapace 0.4.1 ā 0.4.2
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 +2 -2
- package/package.json +1 -1
- package/src/cedar-engine-cedarling.ts +5 -64
- package/src/gui/html.ts +14 -0
- package/src/gui/server.ts +7 -18
- package/src/index.ts +124 -27
- package/src/llm-proxy.ts +0 -21
- package/src/types.ts +4 -1
package/openclaw.plugin.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"id": "carapace",
|
|
3
3
|
"name": "Carapace",
|
|
4
4
|
"description": "Immutable policy boundaries for MCP tool access. Your agent's exoskeleton.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.4.2",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
|
-
"additionalProperties":
|
|
8
|
+
"additionalProperties": true,
|
|
9
9
|
"properties": {
|
|
10
10
|
"guiPort": {
|
|
11
11
|
"type": "number",
|
package/package.json
CHANGED
|
@@ -150,18 +150,14 @@ export class CedarlingEngine {
|
|
|
150
150
|
const typeMatch = request.resource.match(/^(?:\w+::)?(\w+)::/);
|
|
151
151
|
if (typeMatch) resourceEntityType = typeMatch[1];
|
|
152
152
|
|
|
153
|
-
const cedarContext: Record<string, unknown> = { ...(request.context ?? {}) };
|
|
154
|
-
|
|
155
|
-
const effectivePrincipalId = principalId;
|
|
156
|
-
|
|
157
153
|
const result = await this.cedarling.authorize_unsigned({
|
|
158
154
|
principals: [
|
|
159
155
|
{
|
|
160
156
|
cedar_entity_mapping: {
|
|
161
157
|
entity_type: `${this.namespace}::${this.agentEntityType}`,
|
|
162
|
-
id:
|
|
158
|
+
id: principalId,
|
|
163
159
|
},
|
|
164
|
-
name:
|
|
160
|
+
name: principalId,
|
|
165
161
|
},
|
|
166
162
|
],
|
|
167
163
|
action: `${this.namespace}::Action::"${actionName}"`,
|
|
@@ -172,7 +168,7 @@ export class CedarlingEngine {
|
|
|
172
168
|
},
|
|
173
169
|
...(request.context ?? {}),
|
|
174
170
|
},
|
|
175
|
-
context:
|
|
171
|
+
context: request.context ?? {},
|
|
176
172
|
});
|
|
177
173
|
|
|
178
174
|
const decision = result.decision ? "allow" : "deny";
|
|
@@ -500,38 +496,6 @@ export class CedarlingEngine {
|
|
|
500
496
|
},
|
|
501
497
|
},
|
|
502
498
|
},
|
|
503
|
-
Agent: {
|
|
504
|
-
shape: {
|
|
505
|
-
type: "Record",
|
|
506
|
-
attributes: {
|
|
507
|
-
role: {
|
|
508
|
-
type: "EntityOrCommon",
|
|
509
|
-
name: "String",
|
|
510
|
-
required: false,
|
|
511
|
-
},
|
|
512
|
-
parentChain: {
|
|
513
|
-
type: "Set",
|
|
514
|
-
element: { type: "EntityOrCommon", name: "String" },
|
|
515
|
-
required: false,
|
|
516
|
-
},
|
|
517
|
-
issuer: {
|
|
518
|
-
type: "EntityOrCommon",
|
|
519
|
-
name: "String",
|
|
520
|
-
required: false,
|
|
521
|
-
},
|
|
522
|
-
depth: {
|
|
523
|
-
type: "EntityOrCommon",
|
|
524
|
-
name: "Long",
|
|
525
|
-
required: false,
|
|
526
|
-
},
|
|
527
|
-
attestation_proven: {
|
|
528
|
-
type: "EntityOrCommon",
|
|
529
|
-
name: "Boolean",
|
|
530
|
-
required: false,
|
|
531
|
-
},
|
|
532
|
-
},
|
|
533
|
-
},
|
|
534
|
-
},
|
|
535
499
|
Tool: {
|
|
536
500
|
shape: {
|
|
537
501
|
type: "Record",
|
|
@@ -546,21 +510,6 @@ export class CedarlingEngine {
|
|
|
546
510
|
name: "String",
|
|
547
511
|
required: false,
|
|
548
512
|
},
|
|
549
|
-
project: {
|
|
550
|
-
type: "EntityOrCommon",
|
|
551
|
-
name: "String",
|
|
552
|
-
required: false,
|
|
553
|
-
},
|
|
554
|
-
team: {
|
|
555
|
-
type: "EntityOrCommon",
|
|
556
|
-
name: "String",
|
|
557
|
-
required: false,
|
|
558
|
-
},
|
|
559
|
-
domain: {
|
|
560
|
-
type: "EntityOrCommon",
|
|
561
|
-
name: "String",
|
|
562
|
-
required: false,
|
|
563
|
-
},
|
|
564
513
|
},
|
|
565
514
|
},
|
|
566
515
|
},
|
|
@@ -607,17 +556,9 @@ export class CedarlingEngine {
|
|
|
607
556
|
actions: {
|
|
608
557
|
call_tool: {
|
|
609
558
|
appliesTo: {
|
|
610
|
-
principalTypes: [this.agentEntityType
|
|
559
|
+
principalTypes: [this.agentEntityType],
|
|
611
560
|
resourceTypes: ["Tool"],
|
|
612
|
-
context: {
|
|
613
|
-
type: "Record",
|
|
614
|
-
attributes: {
|
|
615
|
-
agent_role: { type: "EntityOrCommon", name: "String", required: false },
|
|
616
|
-
agent_issuer: { type: "EntityOrCommon", name: "String", required: false },
|
|
617
|
-
agent_depth: { type: "EntityOrCommon", name: "Long", required: false },
|
|
618
|
-
agent_attestation_proven: { type: "EntityOrCommon", name: "Boolean", required: false },
|
|
619
|
-
},
|
|
620
|
-
},
|
|
561
|
+
context: { type: "Record", attributes: {} },
|
|
621
562
|
},
|
|
622
563
|
},
|
|
623
564
|
list_tools: {
|
package/src/gui/html.ts
CHANGED
|
@@ -339,6 +339,13 @@ export function guiHtml(): string {
|
|
|
339
339
|
</div>
|
|
340
340
|
</header>
|
|
341
341
|
|
|
342
|
+
<div id="not-enforcing-banner" style="display:none; background:#e8a735; color:#1a1a1a; padding:1rem 1.5rem; font-size:0.95rem; text-align:center; font-weight:500; line-height:1.6;">
|
|
343
|
+
ā ļø Carapace is loaded but <strong>not enforcing</strong>. No tools are gated and the LLM proxy is disabled.
|
|
344
|
+
Your agent is running without policy protection.<br>
|
|
345
|
+
To activate, run these two commands in your terminal:<br>
|
|
346
|
+
<code style="background:rgba(0,0,0,0.1); padding:0.3em 0.6em; border-radius:3px; display:inline-block; margin-top:0.3rem;">openclaw carapace setup && openclaw gateway restart</code>
|
|
347
|
+
</div>
|
|
348
|
+
|
|
342
349
|
<div class="container">
|
|
343
350
|
<div id="servers-section">
|
|
344
351
|
<h2>MCP Servers</h2>
|
|
@@ -533,6 +540,13 @@ export function guiHtml(): string {
|
|
|
533
540
|
function render() {
|
|
534
541
|
document.getElementById('total-count').textContent = state.toolCount ?? state.tools.length;
|
|
535
542
|
document.getElementById('enabled-count').textContent = state.enabledCount ?? state.tools.filter(t => t.enabled).length;
|
|
543
|
+
|
|
544
|
+
// Show/hide not-enforcing banner
|
|
545
|
+
const banner = document.getElementById('not-enforcing-banner');
|
|
546
|
+
if (banner) {
|
|
547
|
+
banner.style.display = state.notEnforcing ? 'block' : 'none';
|
|
548
|
+
}
|
|
549
|
+
|
|
536
550
|
renderServers();
|
|
537
551
|
renderTools();
|
|
538
552
|
renderPolicies();
|
package/src/gui/server.ts
CHANGED
|
@@ -15,6 +15,7 @@ interface GuiOpts {
|
|
|
15
15
|
aggregator: McpAggregator;
|
|
16
16
|
cedar: CedarEngineInterface;
|
|
17
17
|
logger: Logger;
|
|
18
|
+
proxyEnabled?: boolean;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export class ControlGui {
|
|
@@ -23,14 +24,14 @@ export class ControlGui {
|
|
|
23
24
|
private cedar: CedarEngineInterface;
|
|
24
25
|
private logger: Logger;
|
|
25
26
|
private server: Server | null = null;
|
|
26
|
-
|
|
27
|
+
private proxyEnabled: boolean;
|
|
27
28
|
|
|
28
29
|
constructor(opts: GuiOpts) {
|
|
29
30
|
this.port = opts.port;
|
|
30
31
|
this.aggregator = opts.aggregator;
|
|
31
32
|
this.cedar = opts.cedar;
|
|
32
33
|
this.logger = opts.logger;
|
|
33
|
-
|
|
34
|
+
this.proxyEnabled = opts.proxyEnabled ?? false;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
async start(): Promise<void> {
|
|
@@ -62,13 +63,16 @@ export class ControlGui {
|
|
|
62
63
|
if (url.pathname === "/api/status" && req.method === "GET") {
|
|
63
64
|
const servers = this.aggregator.getServerStatus();
|
|
64
65
|
const tools = this.aggregator.listTools();
|
|
66
|
+
const enabledCount = tools.filter((t) => t.enabled).length;
|
|
65
67
|
this.json(res, {
|
|
66
68
|
servers,
|
|
67
69
|
tools,
|
|
68
70
|
policies: this.cedar.getPolicies(),
|
|
69
71
|
toolCount: tools.length,
|
|
70
|
-
enabledCount
|
|
72
|
+
enabledCount,
|
|
71
73
|
defaultPolicy: this.cedar.getDefaultPolicy?.() ?? "allow-all",
|
|
74
|
+
proxyEnabled: this.proxyEnabled,
|
|
75
|
+
notEnforcing: !this.proxyEnabled && enabledCount === 0,
|
|
72
76
|
});
|
|
73
77
|
return;
|
|
74
78
|
}
|
|
@@ -136,21 +140,6 @@ export class ControlGui {
|
|
|
136
140
|
return;
|
|
137
141
|
}
|
|
138
142
|
|
|
139
|
-
if (url.pathname === "/api/agents" && req.method === "GET") {
|
|
140
|
-
// Agent hierarchy removed ā see @clawdreyhepburn/ovid-me for per-agent mandates
|
|
141
|
-
this.json(res, []);
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (url.pathname === "/api/policy-source" && req.method === "GET") {
|
|
146
|
-
// Return all Cedar policies (deployment-wide ceiling)
|
|
147
|
-
const policies = this.cedar.getPolicies();
|
|
148
|
-
const policyText = policies.map(p => p.raw).join("\n\n");
|
|
149
|
-
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
|
|
150
|
-
res.end(policyText || "# No policies defined\n");
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
143
|
// --- GUI ---
|
|
155
144
|
if (url.pathname === "/" || url.pathname === "/index.html") {
|
|
156
145
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
package/src/index.ts
CHANGED
|
@@ -11,8 +11,6 @@ import { ControlGui } from "./gui/server.js";
|
|
|
11
11
|
import { LlmProxy } from "./llm-proxy.js";
|
|
12
12
|
import type { PluginConfig } from "./types.js";
|
|
13
13
|
|
|
14
|
-
export { CarapacePolicySource } from "./policy-source.js";
|
|
15
|
-
export type { PolicySource } from "./policy-source.js";
|
|
16
14
|
export const id = "carapace";
|
|
17
15
|
export const name = "Carapace";
|
|
18
16
|
|
|
@@ -43,6 +41,46 @@ interface OpenClawPluginApi {
|
|
|
43
41
|
registerGatewayMethod?(name: string, handler: (ctx: { respond: (ok: boolean, data: any) => void }) => void): void;
|
|
44
42
|
}
|
|
45
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Build upstream config from either string or object format.
|
|
46
|
+
* String format: proxy.upstream = "https://api.anthropic.com", proxy.apiKey = "sk-..."
|
|
47
|
+
* Object format: proxy.upstream = { anthropic: { url, apiKey }, openai: { url, apiKey } }
|
|
48
|
+
*/
|
|
49
|
+
function buildUpstreamConfig(proxyConfig: NonNullable<PluginConfig["proxy"]>): {
|
|
50
|
+
anthropic?: { url: string; apiKey: string };
|
|
51
|
+
openai?: { url: string; apiKey: string };
|
|
52
|
+
} {
|
|
53
|
+
const upstream = proxyConfig.upstream;
|
|
54
|
+
|
|
55
|
+
if (!upstream) return {};
|
|
56
|
+
|
|
57
|
+
// String format: single upstream URL + flat apiKey
|
|
58
|
+
if (typeof upstream === "string") {
|
|
59
|
+
const apiKey = proxyConfig.apiKey ?? "";
|
|
60
|
+
const url = upstream;
|
|
61
|
+
// Guess provider from URL
|
|
62
|
+
if (url.includes("anthropic")) {
|
|
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 } };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Object format: multi-provider
|
|
72
|
+
return {
|
|
73
|
+
anthropic: upstream.anthropic ? {
|
|
74
|
+
url: upstream.anthropic.url ?? "https://api.anthropic.com",
|
|
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,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
46
84
|
export default function register(api: OpenClawPluginApi) {
|
|
47
85
|
const config: PluginConfig = api.pluginConfig ?? {};
|
|
48
86
|
const logger = api.logger;
|
|
@@ -60,27 +98,20 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
60
98
|
logger,
|
|
61
99
|
});
|
|
62
100
|
|
|
101
|
+
// --- LLM Proxy: intercept tool calls at the API level ---
|
|
102
|
+
const proxyConfig = config.proxy;
|
|
103
|
+
|
|
63
104
|
const gui = new ControlGui({
|
|
64
105
|
port: config.guiPort ?? 19820,
|
|
65
106
|
aggregator,
|
|
66
107
|
cedar,
|
|
67
108
|
logger,
|
|
109
|
+
proxyEnabled: !!proxyConfig?.enabled,
|
|
68
110
|
});
|
|
69
111
|
|
|
70
|
-
|
|
71
|
-
const proxyConfig = config.proxy;
|
|
72
|
-
const proxy = proxyConfig?.enabled ? new LlmProxy({
|
|
112
|
+
let proxy: LlmProxy | null = proxyConfig?.enabled ? new LlmProxy({
|
|
73
113
|
port: proxyConfig.port ?? 19821,
|
|
74
|
-
upstream:
|
|
75
|
-
anthropic: proxyConfig.upstream?.anthropic ? {
|
|
76
|
-
url: proxyConfig.upstream.anthropic.url ?? "https://api.anthropic.com",
|
|
77
|
-
apiKey: proxyConfig.upstream.anthropic.apiKey,
|
|
78
|
-
} : undefined,
|
|
79
|
-
openai: proxyConfig.upstream?.openai ? {
|
|
80
|
-
url: proxyConfig.upstream.openai.url ?? "https://api.openai.com",
|
|
81
|
-
apiKey: proxyConfig.upstream.openai.apiKey,
|
|
82
|
-
} : undefined,
|
|
83
|
-
},
|
|
114
|
+
upstream: buildUpstreamConfig(proxyConfig),
|
|
84
115
|
cedar,
|
|
85
116
|
logger,
|
|
86
117
|
}) : null;
|
|
@@ -132,6 +163,17 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
132
163
|
return { patched: toAdd, alreadyDenied };
|
|
133
164
|
}
|
|
134
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
|
+
|
|
135
177
|
function patchConfigProxyBaseUrl(): { patched: string[]; alreadySet: string[] } {
|
|
136
178
|
const { readFileSync, writeFileSync, existsSync } = require("node:fs");
|
|
137
179
|
const { join } = require("node:path");
|
|
@@ -144,28 +186,45 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
144
186
|
const port = config.proxy?.port ?? 19821;
|
|
145
187
|
const proxyUrl = `http://127.0.0.1:${port}`;
|
|
146
188
|
|
|
147
|
-
// Figure out which providers
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
189
|
+
// Figure out which providers are configured
|
|
190
|
+
const upstreamConfig = proxyConfig ? buildUpstreamConfig(proxyConfig) : {};
|
|
191
|
+
const providers = Object.keys(upstreamConfig).filter(
|
|
192
|
+
(k) => upstreamConfig[k as keyof typeof upstreamConfig],
|
|
193
|
+
);
|
|
151
194
|
|
|
152
195
|
const patched: string[] = [];
|
|
153
196
|
const alreadySet: string[] = [];
|
|
154
197
|
|
|
155
198
|
if (!cfg.models) cfg.models = {};
|
|
199
|
+
if (!cfg.models.mode) cfg.models.mode = "merge";
|
|
156
200
|
if (!cfg.models.providers) cfg.models.providers = {};
|
|
157
201
|
|
|
158
202
|
for (const provider of providers) {
|
|
159
203
|
if (!cfg.models.providers[provider]) cfg.models.providers[provider] = {};
|
|
204
|
+
// Ensure models array exists (OpenClaw requires it)
|
|
205
|
+
if (!Array.isArray(cfg.models.providers[provider].models)) {
|
|
206
|
+
cfg.models.providers[provider].models = [];
|
|
207
|
+
}
|
|
160
208
|
if (cfg.models.providers[provider].baseUrl === proxyUrl) {
|
|
161
209
|
alreadySet.push(provider);
|
|
162
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
|
+
}
|
|
163
215
|
cfg.models.providers[provider].baseUrl = proxyUrl;
|
|
164
216
|
patched.push(provider);
|
|
165
217
|
}
|
|
166
218
|
}
|
|
167
219
|
|
|
168
|
-
|
|
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;
|
|
169
228
|
writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
|
|
170
229
|
}
|
|
171
230
|
|
|
@@ -184,10 +243,27 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
184
243
|
|
|
185
244
|
if (proxy) {
|
|
186
245
|
await proxy.start();
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
246
|
+
|
|
247
|
+
// Health check: verify proxy is actually responding
|
|
248
|
+
const proxyPort = proxyConfig!.port ?? 19821;
|
|
249
|
+
try {
|
|
250
|
+
const controller = new AbortController();
|
|
251
|
+
const timer = setTimeout(() => controller.abort(), 3000);
|
|
252
|
+
const healthResp = await fetch(`http://127.0.0.1:${proxyPort}/health`, { signal: controller.signal });
|
|
253
|
+
clearTimeout(timer);
|
|
254
|
+
if (!healthResp.ok) throw new Error(`HTTP ${healthResp.status}`);
|
|
255
|
+
} catch (err: any) {
|
|
256
|
+
logger.error(`ā Proxy health check failed on port ${proxyPort}: ${err.message}. Disabling proxy.`);
|
|
257
|
+
try { await proxy.stop(); } catch {}
|
|
258
|
+
proxy = null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (proxy) {
|
|
262
|
+
logger.info(
|
|
263
|
+
`š”ļø LLM Proxy active on http://127.0.0.1:${proxyPort} ā ` +
|
|
264
|
+
`all tool calls go through Cedar`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
191
267
|
} else {
|
|
192
268
|
// Check for bypass vulnerabilities only when proxy is disabled
|
|
193
269
|
const bypasses = checkForBypasses();
|
|
@@ -199,6 +275,17 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
199
275
|
);
|
|
200
276
|
}
|
|
201
277
|
}
|
|
278
|
+
|
|
279
|
+
// Warn if Carapace is loaded but not actually enforcing anything
|
|
280
|
+
const tools = aggregator.listTools();
|
|
281
|
+
const enabledCount = tools.filter((t: any) => t.enabled).length;
|
|
282
|
+
if (!proxy && enabledCount === 0) {
|
|
283
|
+
logger.warn(
|
|
284
|
+
`ā ļø Carapace is loaded but NOT ENFORCING. No tools are gated and the LLM proxy is disabled. ` +
|
|
285
|
+
`Your agent is running without policy protection. ` +
|
|
286
|
+
`Run "openclaw carapace setup" to activate enforcement, or configure policies at http://localhost:${config.guiPort ?? 19820}`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
202
289
|
},
|
|
203
290
|
async stop() {
|
|
204
291
|
if (proxy) await proxy.stop();
|
|
@@ -514,6 +601,8 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
514
601
|
.description("Configure OpenClaw to route all traffic through Carapace")
|
|
515
602
|
.action(async () => {
|
|
516
603
|
console.log("\nš¦ Carapace Setup\n");
|
|
604
|
+
backupConfig();
|
|
605
|
+
console.log(" š¦ Backed up openclaw.json ā openclaw.json.carapace-backup");
|
|
517
606
|
let anyChanges = false;
|
|
518
607
|
|
|
519
608
|
// 1. Deny built-in bypass tools
|
|
@@ -545,7 +634,8 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
545
634
|
}
|
|
546
635
|
if (patched.length === 0 && alreadySet.length === 0) {
|
|
547
636
|
console.log(" ā ļø No upstream providers configured in proxy config.");
|
|
548
|
-
console.log(
|
|
637
|
+
console.log(' Set proxy.upstream to a URL string (e.g., "https://api.anthropic.com") with proxy.apiKey,');
|
|
638
|
+
console.log(" or use the object format: proxy.upstream = { anthropic: { apiKey: '...' } }");
|
|
549
639
|
}
|
|
550
640
|
} else {
|
|
551
641
|
console.log("\n LLM proxy not enabled ā skipping baseUrl setup.");
|
|
@@ -599,11 +689,18 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
599
689
|
if (cfg.models?.providers) {
|
|
600
690
|
for (const [name, provCfg] of Object.entries(cfg.models.providers)) {
|
|
601
691
|
if ((provCfg as any)?.baseUrl === proxyUrl) {
|
|
602
|
-
|
|
692
|
+
// Restore original baseUrl if stored
|
|
693
|
+
if ((provCfg as any)._originalBaseUrl) {
|
|
694
|
+
(provCfg as any).baseUrl = (provCfg as any)._originalBaseUrl;
|
|
695
|
+
delete (provCfg as any)._originalBaseUrl;
|
|
696
|
+
console.log(` ā
Restored original baseUrl for ${name}`);
|
|
697
|
+
} else {
|
|
698
|
+
delete (provCfg as any).baseUrl;
|
|
699
|
+
console.log(` ā
Removed baseUrl proxy override for ${name}`);
|
|
700
|
+
}
|
|
603
701
|
// Clean up empty objects
|
|
604
702
|
if (Object.keys(provCfg as any).length === 0) delete cfg.models.providers[name];
|
|
605
703
|
changed = true;
|
|
606
|
-
console.log(` ā
Removed baseUrl proxy override for ${name}`);
|
|
607
704
|
console.log(` ${name} will connect directly to its API again.`);
|
|
608
705
|
}
|
|
609
706
|
}
|
package/src/llm-proxy.ts
CHANGED
|
@@ -63,14 +63,6 @@ export class LlmProxy {
|
|
|
63
63
|
toolCallsDenied: 0,
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
-
// Audit log for GUI display
|
|
67
|
-
private auditLog: Array<{
|
|
68
|
-
timestamp: number;
|
|
69
|
-
tool: string;
|
|
70
|
-
decision: "allow" | "deny";
|
|
71
|
-
reasons: string[];
|
|
72
|
-
}> = [];
|
|
73
|
-
|
|
74
66
|
constructor(opts: LlmProxyOpts) {
|
|
75
67
|
this.port = opts.port;
|
|
76
68
|
this.upstream = opts.upstream;
|
|
@@ -78,10 +70,6 @@ export class LlmProxy {
|
|
|
78
70
|
this.logger = opts.logger;
|
|
79
71
|
}
|
|
80
72
|
|
|
81
|
-
getAuditLog() {
|
|
82
|
-
return this.auditLog.slice(-100); // last 100 entries
|
|
83
|
-
}
|
|
84
|
-
|
|
85
73
|
async start(): Promise<void> {
|
|
86
74
|
this.server = createServer(async (req, res) => {
|
|
87
75
|
try {
|
|
@@ -608,15 +596,6 @@ export class LlmProxy {
|
|
|
608
596
|
context,
|
|
609
597
|
});
|
|
610
598
|
|
|
611
|
-
// Audit log entry
|
|
612
|
-
this.auditLog.push({
|
|
613
|
-
timestamp: Date.now(),
|
|
614
|
-
tool: toolName,
|
|
615
|
-
decision: decision.decision,
|
|
616
|
-
reasons: decision.reasons,
|
|
617
|
-
});
|
|
618
|
-
if (this.auditLog.length > 500) this.auditLog.splice(0, this.auditLog.length - 100);
|
|
619
|
-
|
|
620
599
|
if (decision.decision === "deny") {
|
|
621
600
|
this.stats.toolCallsDenied++;
|
|
622
601
|
}
|
package/src/types.ts
CHANGED
|
@@ -27,10 +27,13 @@ export interface PluginConfig {
|
|
|
27
27
|
proxy?: {
|
|
28
28
|
enabled?: boolean;
|
|
29
29
|
port?: number; // default: 19821
|
|
30
|
-
|
|
30
|
+
/** String (simple: base URL) or object (multi-provider) */
|
|
31
|
+
upstream?: string | {
|
|
31
32
|
anthropic?: { url?: string; apiKey: string };
|
|
32
33
|
openai?: { url?: string; apiKey: string };
|
|
33
34
|
};
|
|
35
|
+
/** API key for the upstream provider (used with string upstream) */
|
|
36
|
+
apiKey?: string;
|
|
34
37
|
};
|
|
35
38
|
}
|
|
36
39
|
|