@clawdreyhepburn/carapace 0.4.0 → 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.
@@ -3,8 +3,13 @@
3
3
  * so that OVID-ME can query the effective policy ceiling.
4
4
  */
5
5
  /**
6
- * Interface matching @clawdreyhepburn/ovid-me's PolicySource.
7
- * Defined locally to avoid circular dependencies.
6
+ * Must match @clawdreyhepburn/ovid-me PolicySource interface.
7
+ *
8
+ * Kept as a local copy because ovid-me pulls in native dependencies
9
+ * (better-sqlite3) that would bloat Carapace's install. A type
10
+ * compatibility test in test/policy-source.test.ts guards against drift.
11
+ *
12
+ * Canonical definition: ovid-me/src/config.ts
8
13
  */
9
14
  export interface PolicySource {
10
15
  getEffectivePolicy(principal: string): Promise<string | null>;
@@ -1 +1 @@
1
- {"version":3,"file":"policy-source.js","sourceRoot":"","sources":["../src/policy-source.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAChE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAUlC;;;;GAIG;AACH,MAAM,OAAO,oBAAoB;IACvB,SAAS,CAAS;IAE1B,YAAY,SAAkB;QAC5B,IAAI,CAAC,SAAS,GAAG,CAAC,SAAS,IAAI,2BAA2B,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC;IACtF,CAAC;IAED,KAAK,CAAC,kBAAkB,CAAC,UAAkB;QACzC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC;YAAE,OAAO,IAAI,CAAC;QAE7C,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;QAC5E,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAEpC,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;QAChF,OAAO,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC/B,CAAC;CACF"}
1
+ {"version":3,"file":"policy-source.js","sourceRoot":"","sources":["../src/policy-source.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAChE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAelC;;;;GAIG;AACH,MAAM,OAAO,oBAAoB;IACvB,SAAS,CAAS;IAE1B,YAAY,SAAkB;QAC5B,IAAI,CAAC,SAAS,GAAG,CAAC,SAAS,IAAI,2BAA2B,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC;IACtF,CAAC;IAED,KAAK,CAAC,kBAAkB,CAAC,UAAkB;QACzC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC;YAAE,OAAO,IAAI,CAAC;QAE7C,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;QAC5E,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAEpC,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;QAChF,OAAO,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC/B,CAAC;CACF"}
@@ -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.2",
6
6
  "configSchema": {
7
7
  "type": "object",
8
- "additionalProperties": false,
8
+ "additionalProperties": true,
9
9
  "properties": {
10
10
  "guiPort": {
11
11
  "type": "number",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawdreyhepburn/carapace",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
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,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 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
 
@@ -184,10 +243,27 @@ export default function register(api: OpenClawPluginApi) {
184
243
 
185
244
  if (proxy) {
186
245
  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
- );
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(" Add proxy.upstream.anthropic or proxy.upstream.openai to your plugin config.");
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
- delete (provCfg as any).baseUrl;
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
  }
@@ -8,8 +8,13 @@ import { join } from "node:path";
8
8
  import { homedir } from "node:os";
9
9
 
10
10
  /**
11
- * Interface matching @clawdreyhepburn/ovid-me's PolicySource.
12
- * Defined locally to avoid circular dependencies.
11
+ * Must match @clawdreyhepburn/ovid-me PolicySource interface.
12
+ *
13
+ * Kept as a local copy because ovid-me pulls in native dependencies
14
+ * (better-sqlite3) that would bloat Carapace's install. A type
15
+ * compatibility test in test/policy-source.test.ts guards against drift.
16
+ *
17
+ * Canonical definition: ovid-me/src/config.ts
13
18
  */
14
19
  export interface PolicySource {
15
20
  getEffectivePolicy(principal: string): Promise<string | null>;
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