@clawdreyhepburn/carapace 0.4.1 → 0.4.3

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.
@@ -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.3.0",
5
+ "version": "0.4.3",
6
6
  "configSchema": {
7
7
  "type": "object",
8
- "additionalProperties": false,
8
+ "additionalProperties": true,
9
9
  "properties": {
10
10
  "guiPort": {
11
11
  "type": "number",
@@ -20,13 +20,31 @@
20
20
  "properties": {
21
21
  "transport": {
22
22
  "type": "string",
23
- "enum": ["stdio", "http", "sse"],
23
+ "enum": [
24
+ "stdio",
25
+ "http",
26
+ "sse"
27
+ ],
24
28
  "default": "stdio"
25
29
  },
26
- "command": { "type": "string" },
27
- "args": { "type": "array", "items": { "type": "string" } },
28
- "env": { "type": "object", "additionalProperties": { "type": "string" } },
29
- "url": { "type": "string" }
30
+ "command": {
31
+ "type": "string"
32
+ },
33
+ "args": {
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
+ }
30
48
  }
31
49
  }
32
50
  },
@@ -37,7 +55,10 @@
37
55
  },
38
56
  "defaultPolicy": {
39
57
  "type": "string",
40
- "enum": ["deny-all", "allow-all"],
58
+ "enum": [
59
+ "deny-all",
60
+ "allow-all"
61
+ ],
41
62
  "default": "allow-all",
42
63
  "description": "Default policy for tools. allow-all (default) keeps everything working — use the GUI to restrict. deny-all requires explicit permits."
43
64
  },
@@ -74,13 +95,32 @@
74
95
  }
75
96
  },
76
97
  "uiHints": {
77
- "guiPort": { "label": "GUI Port", "placeholder": "19820" },
78
- "policyDir": { "label": "Policy Directory" },
79
- "defaultPolicy": { "label": "Default Policy for New Tools" },
80
- "verify": { "label": "Enable Formal Verification" },
81
- "proxy.enabled": { "label": "Enable LLM Proxy" },
82
- "proxy.port": { "label": "Proxy Port", "placeholder": "19821" },
83
- "proxy.upstream": { "label": "Upstream API URL" },
84
- "proxy.apiKey": { "label": "Upstream API Key", "sensitive": true }
98
+ "guiPort": {
99
+ "label": "GUI Port",
100
+ "placeholder": "19820"
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
+ }
85
125
  }
86
126
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawdreyhepburn/carapace",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Immutable policy boundaries for MCP tool access. Powered by Cedar + Cedarling WASM.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -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: effectivePrincipalId,
158
+ id: principalId,
163
159
  },
164
- name: effectivePrincipalId,
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: cedarContext,
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, "Agent"],
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: tools.filter((t) => t.enabled).length,
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
- // --- LLM Proxy: intercept tool calls at the API level ---
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,31 +186,83 @@ 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 have upstream keys configured
148
- const providers: string[] = [];
149
- if (config.proxy?.upstream?.anthropic) providers.push("anthropic");
150
- if (config.proxy?.upstream?.openai) providers.push("openai");
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
- if (patched.length > 0) {
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
 
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}`);
263
+ }
264
+ }
265
+
172
266
  return { patched, alreadySet };
173
267
  }
174
268
 
@@ -184,10 +278,27 @@ export default function register(api: OpenClawPluginApi) {
184
278
 
185
279
  if (proxy) {
186
280
  await proxy.start();
187
- logger.info(
188
- `🛡️ LLM Proxy active on http://127.0.0.1:${proxyConfig!.port ?? 19821} — ` +
189
- `all tool calls go through Cedar`
190
- );
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
+ }
191
302
  } else {
192
303
  // Check for bypass vulnerabilities only when proxy is disabled
193
304
  const bypasses = checkForBypasses();
@@ -199,6 +310,17 @@ export default function register(api: OpenClawPluginApi) {
199
310
  );
200
311
  }
201
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) {
318
+ logger.warn(
319
+ `⚠️ Carapace is loaded but NOT ENFORCING. No tools are gated and the LLM proxy is disabled. ` +
320
+ `Your agent is running without policy protection. ` +
321
+ `Run "openclaw carapace setup" to activate enforcement, or configure policies at http://localhost:${config.guiPort ?? 19820}`
322
+ );
323
+ }
202
324
  },
203
325
  async stop() {
204
326
  if (proxy) await proxy.stop();
@@ -514,6 +636,8 @@ export default function register(api: OpenClawPluginApi) {
514
636
  .description("Configure OpenClaw to route all traffic through Carapace")
515
637
  .action(async () => {
516
638
  console.log("\n🦞 Carapace Setup\n");
639
+ backupConfig();
640
+ console.log(" 📦 Backed up openclaw.json → openclaw.json.carapace-backup");
517
641
  let anyChanges = false;
518
642
 
519
643
  // 1. Deny built-in bypass tools
@@ -545,7 +669,8 @@ export default function register(api: OpenClawPluginApi) {
545
669
  }
546
670
  if (patched.length === 0 && alreadySet.length === 0) {
547
671
  console.log(" ⚠️ No upstream providers configured in proxy config.");
548
- console.log(" Add proxy.upstream.anthropic or proxy.upstream.openai to your plugin config.");
672
+ console.log(' Set proxy.upstream to a URL string (e.g., "https://api.anthropic.com") with proxy.apiKey,');
673
+ console.log(" or use the object format: proxy.upstream = { anthropic: { apiKey: '...' } }");
549
674
  }
550
675
  } else {
551
676
  console.log("\n LLM proxy not enabled — skipping baseUrl setup.");
@@ -599,11 +724,18 @@ export default function register(api: OpenClawPluginApi) {
599
724
  if (cfg.models?.providers) {
600
725
  for (const [name, provCfg] of Object.entries(cfg.models.providers)) {
601
726
  if ((provCfg as any)?.baseUrl === proxyUrl) {
602
- delete (provCfg as any).baseUrl;
727
+ // Restore original baseUrl if stored
728
+ if ((provCfg as any)._originalBaseUrl) {
729
+ (provCfg as any).baseUrl = (provCfg as any)._originalBaseUrl;
730
+ delete (provCfg as any)._originalBaseUrl;
731
+ console.log(` ✅ Restored original baseUrl for ${name}`);
732
+ } else {
733
+ delete (provCfg as any).baseUrl;
734
+ console.log(` ✅ Removed baseUrl proxy override for ${name}`);
735
+ }
603
736
  // Clean up empty objects
604
737
  if (Object.keys(provCfg as any).length === 0) delete cfg.models.providers[name];
605
738
  changed = true;
606
- console.log(` ✅ Removed baseUrl proxy override for ${name}`);
607
739
  console.log(` ${name} will connect directly to its API again.`);
608
740
  }
609
741
  }
@@ -611,6 +743,68 @@ export default function register(api: OpenClawPluginApi) {
611
743
  if (cfg.models && Object.keys(cfg.models).length === 0) delete cfg.models;
612
744
  }
613
745
 
746
+ // Also clean up the per-agent models.json
747
+ const agentModelsPath = join(homedir(), ".openclaw", "agents", "main", "agent", "models.json");
748
+ if (existsSync(agentModelsPath)) {
749
+ try {
750
+ const agentModels = JSON.parse(readFileSync(agentModelsPath, "utf-8"));
751
+ let modelsChanged = false;
752
+ if (agentModels.providers) {
753
+ for (const [name, provCfg] of Object.entries(agentModels.providers)) {
754
+ if ((provCfg as any)?.baseUrl === proxyUrl) {
755
+ if ((provCfg as any)._originalBaseUrl) {
756
+ (provCfg as any).baseUrl = (provCfg as any)._originalBaseUrl;
757
+ delete (provCfg as any)._originalBaseUrl;
758
+ } else {
759
+ delete (provCfg as any).baseUrl;
760
+ }
761
+ if (Object.keys(provCfg as any).length === 0) delete agentModels.providers[name];
762
+ modelsChanged = true;
763
+ console.log(` ✅ Cleaned proxy baseUrl from models.json for ${name}`);
764
+ }
765
+ }
766
+ }
767
+ if (modelsChanged) {
768
+ writeFileSync(agentModelsPath, JSON.stringify(agentModels, null, 2) + "\n", "utf-8");
769
+ changed = true;
770
+ }
771
+ } catch (e: any) {
772
+ console.log(` ⚠️ Could not clean models.json: ${e.message}`);
773
+ }
774
+ }
775
+
776
+ // Clean up per-agent models.json (this is what actually routes API calls)
777
+ const agentModelsPath = join(homedir(), ".openclaw", "agents", "main", "agent", "models.json");
778
+ if (existsSync(agentModelsPath)) {
779
+ try {
780
+ const agentModels = JSON.parse(readFileSync(agentModelsPath, "utf-8"));
781
+ let modelsChanged = false;
782
+ if (agentModels.providers) {
783
+ for (const [name, provCfg] of Object.entries(agentModels.providers)) {
784
+ if ((provCfg as any)?.baseUrl === proxyUrl) {
785
+ if ((provCfg as any)._originalBaseUrl) {
786
+ (provCfg as any).baseUrl = (provCfg as any)._originalBaseUrl;
787
+ delete (provCfg as any)._originalBaseUrl;
788
+ } else {
789
+ delete (provCfg as any).baseUrl;
790
+ }
791
+ // Remove empty provider entries (but keep ones with other config)
792
+ const remaining = Object.keys(provCfg as any).filter(k => k !== 'models' || (provCfg as any).models?.length > 0);
793
+ if (remaining.length === 0) delete agentModels.providers[name];
794
+ modelsChanged = true;
795
+ console.log(` ✅ Cleaned proxy baseUrl from models.json (${name})`);
796
+ }
797
+ }
798
+ }
799
+ if (modelsChanged) {
800
+ writeFileSync(agentModelsPath, JSON.stringify(agentModels, null, 2) + "\n", "utf-8");
801
+ changed = true;
802
+ }
803
+ } catch (e: any) {
804
+ console.log(` ⚠️ Could not clean models.json: ${e.message}`);
805
+ }
806
+ }
807
+
614
808
  // Disable the plugin entry (don't delete — user might want to re-enable)
615
809
  if (cfg.plugins?.entries?.carapace?.enabled) {
616
810
  cfg.plugins.entries.carapace.enabled = false;
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
- upstream?: {
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