@drakon-systems/shieldcortex-realtime 4.47.12 → 4.47.14
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/dist/index.js +150 -8
- package/dist/openclaw.plugin.json +1 -1
- package/index.ts +180 -10
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -143,6 +143,15 @@ export function __setRuntimeForTest(runtime) {
|
|
|
143
143
|
if (runtime)
|
|
144
144
|
runtimePromise = null;
|
|
145
145
|
}
|
|
146
|
+
export function __resetConfigStateForTest() {
|
|
147
|
+
_config = null;
|
|
148
|
+
_configOverride = null;
|
|
149
|
+
_lastShieldConfigRef = null;
|
|
150
|
+
_registered = false;
|
|
151
|
+
}
|
|
152
|
+
const INTERCEPT_SEVERITIES = ['low', 'medium', 'high', 'critical'];
|
|
153
|
+
const INTERCEPT_ACTIONS = ['log', 'warn', 'require_approval'];
|
|
154
|
+
const FAILURE_ACTIONS = ['allow', 'deny'];
|
|
146
155
|
const PLUGIN_ID = "shieldcortex-realtime";
|
|
147
156
|
const PLUGIN_PACKAGE_NAME = "@drakon-systems/shieldcortex-realtime";
|
|
148
157
|
const PLUGIN_CONFIG_UI_HINTS = {
|
|
@@ -183,6 +192,57 @@ const PLUGIN_CONFIG_UI_HINTS = {
|
|
|
183
192
|
help: "How many recent extracted memories to keep in the dedupe cache.",
|
|
184
193
|
advanced: true,
|
|
185
194
|
},
|
|
195
|
+
"interceptor.enabled": {
|
|
196
|
+
label: "Enable Tool Call Interceptor",
|
|
197
|
+
help: "Scan memory-write tool calls and gate suspicious content behind user approval.",
|
|
198
|
+
},
|
|
199
|
+
"interceptor.actionGuard.enabled": {
|
|
200
|
+
label: "Action Guard",
|
|
201
|
+
help: "Gate dangerous shell/file/network/git tool calls before they execute. Catastrophic operations are always blocked while enabled.",
|
|
202
|
+
},
|
|
203
|
+
"interceptor.actionGuard.enforce": {
|
|
204
|
+
label: "Enforce Action Guard",
|
|
205
|
+
help: "Enforce dangerous-operation gating (attended: approval prompt; unattended: failurePolicy). Off = warn-and-allow (advisory). Catastrophic operations block regardless.",
|
|
206
|
+
},
|
|
207
|
+
"interceptor.actionGuard.autoApprove": {
|
|
208
|
+
label: "Action Guard Auto-approve List",
|
|
209
|
+
help: "Family/action/signal names pre-approved for unattended agents that legitimately need specific dangerous operations.",
|
|
210
|
+
advanced: true,
|
|
211
|
+
},
|
|
212
|
+
"interceptor.actionGuard.auditAllows": {
|
|
213
|
+
label: "Audit Recognised Allows",
|
|
214
|
+
help: "Write an audit entry when the guard evaluates a recognised operation and allows it.",
|
|
215
|
+
advanced: true,
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
const SEVERITY_ACTION_SCHEMA = {
|
|
219
|
+
type: "object",
|
|
220
|
+
additionalProperties: false,
|
|
221
|
+
properties: Object.fromEntries(INTERCEPT_SEVERITIES.map((severity) => [severity, { type: "string", enum: [...INTERCEPT_ACTIONS] }])),
|
|
222
|
+
};
|
|
223
|
+
const FAILURE_POLICY_SCHEMA = {
|
|
224
|
+
type: "object",
|
|
225
|
+
additionalProperties: false,
|
|
226
|
+
properties: Object.fromEntries(INTERCEPT_SEVERITIES.map((severity) => [severity, { type: "string", enum: [...FAILURE_ACTIONS] }])),
|
|
227
|
+
};
|
|
228
|
+
const INTERCEPTOR_JSON_SCHEMA = {
|
|
229
|
+
type: "object",
|
|
230
|
+
additionalProperties: false,
|
|
231
|
+
properties: {
|
|
232
|
+
enabled: { type: "boolean" },
|
|
233
|
+
severityActions: SEVERITY_ACTION_SCHEMA,
|
|
234
|
+
failurePolicy: FAILURE_POLICY_SCHEMA,
|
|
235
|
+
actionGuard: {
|
|
236
|
+
type: "object",
|
|
237
|
+
additionalProperties: false,
|
|
238
|
+
properties: {
|
|
239
|
+
enabled: { type: "boolean" },
|
|
240
|
+
enforce: { type: "boolean" },
|
|
241
|
+
autoApprove: { type: "array", items: { type: "string" } },
|
|
242
|
+
auditAllows: { type: "boolean" },
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
},
|
|
186
246
|
};
|
|
187
247
|
const PLUGIN_CONFIG_JSON_SCHEMA = {
|
|
188
248
|
type: "object",
|
|
@@ -196,6 +256,9 @@ const PLUGIN_CONFIG_JSON_SCHEMA = {
|
|
|
196
256
|
openclawAutoMemoryDedupe: { type: "boolean" },
|
|
197
257
|
openclawAutoMemoryNoveltyThreshold: { type: "number", minimum: 0.6, maximum: 0.99 },
|
|
198
258
|
openclawAutoMemoryMaxRecent: { type: "integer", minimum: 50, maximum: 1000 },
|
|
259
|
+
// #112: without this, `additionalProperties: false` declared the whole
|
|
260
|
+
// interceptor block invalid — mirror openclaw.plugin.json's configSchema.
|
|
261
|
+
interceptor: INTERCEPTOR_JSON_SCHEMA,
|
|
199
262
|
},
|
|
200
263
|
};
|
|
201
264
|
let _config = null;
|
|
@@ -256,8 +319,89 @@ function normaliseConfig(raw) {
|
|
|
256
319
|
if (typeof value.openclawAutoMemoryMaxRecent === "number" && !Number.isNaN(value.openclawAutoMemoryMaxRecent)) {
|
|
257
320
|
config.openclawAutoMemoryMaxRecent = Math.floor(clamp(value.openclawAutoMemoryMaxRecent, 50, 1000));
|
|
258
321
|
}
|
|
322
|
+
// #112: the allowlist above predates the interceptor and silently dropped
|
|
323
|
+
// the entire nested `interceptor` object — `interceptor.enabled: false` was
|
|
324
|
+
// ignored and DEFAULT_INTERCEPTOR_CONFIG re-armed the before_tool_call gate.
|
|
325
|
+
// Validate-and-preserve every nested interceptor key the manifest schema
|
|
326
|
+
// accepts; invalid values are dropped individually, valid siblings survive.
|
|
327
|
+
const interceptor = normaliseInterceptorConfig(value.interceptor);
|
|
328
|
+
if (interceptor)
|
|
329
|
+
config.interceptor = interceptor;
|
|
259
330
|
return config;
|
|
260
331
|
}
|
|
332
|
+
function normaliseSeverityMap(raw, allowed) {
|
|
333
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
334
|
+
return undefined;
|
|
335
|
+
const value = raw;
|
|
336
|
+
const out = {};
|
|
337
|
+
for (const severity of INTERCEPT_SEVERITIES) {
|
|
338
|
+
const entry = value[severity];
|
|
339
|
+
if (typeof entry === "string" && allowed.includes(entry)) {
|
|
340
|
+
out[severity] = entry;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
344
|
+
}
|
|
345
|
+
function normaliseInterceptorConfig(raw) {
|
|
346
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
347
|
+
return undefined;
|
|
348
|
+
const value = raw;
|
|
349
|
+
const out = {};
|
|
350
|
+
if (typeof value.enabled === "boolean")
|
|
351
|
+
out.enabled = value.enabled;
|
|
352
|
+
const severityActions = normaliseSeverityMap(value.severityActions, INTERCEPT_ACTIONS);
|
|
353
|
+
if (severityActions)
|
|
354
|
+
out.severityActions = severityActions;
|
|
355
|
+
const failurePolicy = normaliseSeverityMap(value.failurePolicy, FAILURE_ACTIONS);
|
|
356
|
+
if (failurePolicy)
|
|
357
|
+
out.failurePolicy = failurePolicy;
|
|
358
|
+
if (value.actionGuard && typeof value.actionGuard === "object" && !Array.isArray(value.actionGuard)) {
|
|
359
|
+
const rawGuard = value.actionGuard;
|
|
360
|
+
const guard = {};
|
|
361
|
+
if (typeof rawGuard.enabled === "boolean")
|
|
362
|
+
guard.enabled = rawGuard.enabled;
|
|
363
|
+
if (typeof rawGuard.enforce === "boolean")
|
|
364
|
+
guard.enforce = rawGuard.enforce;
|
|
365
|
+
if (typeof rawGuard.auditAllows === "boolean")
|
|
366
|
+
guard.auditAllows = rawGuard.auditAllows;
|
|
367
|
+
if (Array.isArray(rawGuard.autoApprove) && rawGuard.autoApprove.every((entry) => typeof entry === "string")) {
|
|
368
|
+
guard.autoApprove = rawGuard.autoApprove;
|
|
369
|
+
}
|
|
370
|
+
if (Object.keys(guard).length > 0)
|
|
371
|
+
out.actionGuard = guard;
|
|
372
|
+
}
|
|
373
|
+
return out;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Merge two normalised configs (#112). Semantics:
|
|
377
|
+
* - Top-level scalar keys: override wins when present.
|
|
378
|
+
* - `interceptor` deep-merges PER KEY — per severity entry, per guard flag —
|
|
379
|
+
* so an override that sets one nested key does not wholesale-discard the
|
|
380
|
+
* base's other interceptor settings.
|
|
381
|
+
* - Explicit values, including `false`, always win over base values; absent
|
|
382
|
+
* keys fall through to the base.
|
|
383
|
+
* - Defaults are NOT applied here: DEFAULT_INTERCEPTOR_CONFIG only fills the
|
|
384
|
+
* remaining gaps in initInterceptor(), so it can never override an explicit
|
|
385
|
+
* user value.
|
|
386
|
+
*/
|
|
387
|
+
function mergeConfigs(base, override) {
|
|
388
|
+
const merged = { ...base, ...override };
|
|
389
|
+
if (base.interceptor || override.interceptor) {
|
|
390
|
+
const b = base.interceptor ?? {};
|
|
391
|
+
const o = override.interceptor ?? {};
|
|
392
|
+
merged.interceptor = { ...b, ...o };
|
|
393
|
+
if (b.severityActions || o.severityActions) {
|
|
394
|
+
merged.interceptor.severityActions = { ...b.severityActions, ...o.severityActions };
|
|
395
|
+
}
|
|
396
|
+
if (b.failurePolicy || o.failurePolicy) {
|
|
397
|
+
merged.interceptor.failurePolicy = { ...b.failurePolicy, ...o.failurePolicy };
|
|
398
|
+
}
|
|
399
|
+
if (b.actionGuard || o.actionGuard) {
|
|
400
|
+
merged.interceptor.actionGuard = { ...b.actionGuard, ...o.actionGuard };
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return merged;
|
|
404
|
+
}
|
|
261
405
|
function extractPluginConfig(rootConfig) {
|
|
262
406
|
if (!rootConfig || typeof rootConfig !== "object" || Array.isArray(rootConfig))
|
|
263
407
|
return {};
|
|
@@ -276,10 +420,7 @@ function applyPluginConfigOverride(api) {
|
|
|
276
420
|
const pluginConfig = extractPluginConfig(runtimeConfig);
|
|
277
421
|
if (Object.keys(pluginConfig).length === 0)
|
|
278
422
|
return;
|
|
279
|
-
_configOverride = {
|
|
280
|
-
...(_configOverride ?? {}),
|
|
281
|
-
...pluginConfig,
|
|
282
|
-
};
|
|
423
|
+
_configOverride = mergeConfigs(_configOverride ?? {}, pluginConfig);
|
|
283
424
|
// Override changed — invalidate so loadConfig() re-merges with new override.
|
|
284
425
|
_config = null;
|
|
285
426
|
_lastShieldConfigRef = null;
|
|
@@ -289,10 +430,9 @@ async function loadConfig() {
|
|
|
289
430
|
if (_config && shieldConfigRaw === _lastShieldConfigRef)
|
|
290
431
|
return _config;
|
|
291
432
|
_lastShieldConfigRef = shieldConfigRaw;
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
};
|
|
433
|
+
// Plugin config (openclaw.json) deep-merges over the shield config file —
|
|
434
|
+
// see mergeConfigs() for the per-key semantics.
|
|
435
|
+
_config = mergeConfigs(normaliseConfig(shieldConfigRaw), _configOverride ?? {});
|
|
296
436
|
return _config;
|
|
297
437
|
}
|
|
298
438
|
function isAutoMemoryEnabled(config) {
|
|
@@ -727,6 +867,8 @@ export default {
|
|
|
727
867
|
interceptorInitAttempted = true;
|
|
728
868
|
try {
|
|
729
869
|
const scConfig = await loadConfig();
|
|
870
|
+
// Normalised user config (deep-partial); DEFAULT_INTERCEPTOR_CONFIG
|
|
871
|
+
// fills the gaps below — defaults never override explicit values.
|
|
730
872
|
const rawInterceptorConfig = scConfig.interceptor;
|
|
731
873
|
const interceptorConfig = {
|
|
732
874
|
...DEFAULT_INTERCEPTOR_CONFIG,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shieldcortex-realtime",
|
|
3
|
-
"version": "4.47.
|
|
3
|
+
"version": "4.47.14",
|
|
4
4
|
"name": "ShieldCortex Real-time Scanner",
|
|
5
5
|
"description": "Real-time defence scanning on LLM input, memory extraction on LLM output, and active tool call interception with approval gating.",
|
|
6
6
|
"kind": null,
|
package/index.ts
CHANGED
|
@@ -178,6 +178,12 @@ export function __setRuntimeForTest(runtime: OpenClawRuntime | null): void {
|
|
|
178
178
|
_runtimeOverride = runtime;
|
|
179
179
|
if (runtime) runtimePromise = null;
|
|
180
180
|
}
|
|
181
|
+
export function __resetConfigStateForTest(): void {
|
|
182
|
+
_config = null;
|
|
183
|
+
_configOverride = null;
|
|
184
|
+
_lastShieldConfigRef = null;
|
|
185
|
+
_registered = false;
|
|
186
|
+
}
|
|
181
187
|
|
|
182
188
|
type LlmInputEvent = {
|
|
183
189
|
runId: string; sessionId: string; provider: string; model: string;
|
|
@@ -217,6 +223,27 @@ type PluginApi = {
|
|
|
217
223
|
|
|
218
224
|
// ==================== CONFIG ====================
|
|
219
225
|
|
|
226
|
+
// User-facing interceptor config: a deep-partial of InterceptorConfig. Every
|
|
227
|
+
// key is optional — DEFAULT_INTERCEPTOR_CONFIG fills the gaps in
|
|
228
|
+
// initInterceptor(), never here, so explicit values (including `false`) always
|
|
229
|
+
// win over defaults.
|
|
230
|
+
type InterceptSeverity = 'low' | 'medium' | 'high' | 'critical';
|
|
231
|
+
const INTERCEPT_SEVERITIES: readonly InterceptSeverity[] = ['low', 'medium', 'high', 'critical'];
|
|
232
|
+
const INTERCEPT_ACTIONS = ['log', 'warn', 'require_approval'] as const;
|
|
233
|
+
const FAILURE_ACTIONS = ['allow', 'deny'] as const;
|
|
234
|
+
|
|
235
|
+
interface InterceptorUserConfig {
|
|
236
|
+
enabled?: boolean;
|
|
237
|
+
severityActions?: Partial<Record<InterceptSeverity, (typeof INTERCEPT_ACTIONS)[number]>>;
|
|
238
|
+
failurePolicy?: Partial<Record<InterceptSeverity, (typeof FAILURE_ACTIONS)[number]>>;
|
|
239
|
+
actionGuard?: {
|
|
240
|
+
enabled?: boolean;
|
|
241
|
+
enforce?: boolean;
|
|
242
|
+
autoApprove?: string[];
|
|
243
|
+
auditAllows?: boolean;
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
220
247
|
interface SCConfig {
|
|
221
248
|
cloudEnabled?: boolean;
|
|
222
249
|
cloudApiKey?: string;
|
|
@@ -226,6 +253,7 @@ interface SCConfig {
|
|
|
226
253
|
openclawAutoMemoryDedupe?: boolean;
|
|
227
254
|
openclawAutoMemoryNoveltyThreshold?: number;
|
|
228
255
|
openclawAutoMemoryMaxRecent?: number;
|
|
256
|
+
interceptor?: InterceptorUserConfig;
|
|
229
257
|
}
|
|
230
258
|
|
|
231
259
|
const PLUGIN_ID = "shieldcortex-realtime";
|
|
@@ -268,8 +296,66 @@ const PLUGIN_CONFIG_UI_HINTS = {
|
|
|
268
296
|
help: "How many recent extracted memories to keep in the dedupe cache.",
|
|
269
297
|
advanced: true,
|
|
270
298
|
},
|
|
299
|
+
"interceptor.enabled": {
|
|
300
|
+
label: "Enable Tool Call Interceptor",
|
|
301
|
+
help: "Scan memory-write tool calls and gate suspicious content behind user approval.",
|
|
302
|
+
},
|
|
303
|
+
"interceptor.actionGuard.enabled": {
|
|
304
|
+
label: "Action Guard",
|
|
305
|
+
help: "Gate dangerous shell/file/network/git tool calls before they execute. Catastrophic operations are always blocked while enabled.",
|
|
306
|
+
},
|
|
307
|
+
"interceptor.actionGuard.enforce": {
|
|
308
|
+
label: "Enforce Action Guard",
|
|
309
|
+
help: "Enforce dangerous-operation gating (attended: approval prompt; unattended: failurePolicy). Off = warn-and-allow (advisory). Catastrophic operations block regardless.",
|
|
310
|
+
},
|
|
311
|
+
"interceptor.actionGuard.autoApprove": {
|
|
312
|
+
label: "Action Guard Auto-approve List",
|
|
313
|
+
help: "Family/action/signal names pre-approved for unattended agents that legitimately need specific dangerous operations.",
|
|
314
|
+
advanced: true,
|
|
315
|
+
},
|
|
316
|
+
"interceptor.actionGuard.auditAllows": {
|
|
317
|
+
label: "Audit Recognised Allows",
|
|
318
|
+
help: "Write an audit entry when the guard evaluates a recognised operation and allows it.",
|
|
319
|
+
advanced: true,
|
|
320
|
+
},
|
|
271
321
|
} as const;
|
|
272
322
|
|
|
323
|
+
const SEVERITY_ACTION_SCHEMA = {
|
|
324
|
+
type: "object",
|
|
325
|
+
additionalProperties: false,
|
|
326
|
+
properties: Object.fromEntries(
|
|
327
|
+
INTERCEPT_SEVERITIES.map((severity) => [severity, { type: "string", enum: [...INTERCEPT_ACTIONS] }]),
|
|
328
|
+
),
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const FAILURE_POLICY_SCHEMA = {
|
|
332
|
+
type: "object",
|
|
333
|
+
additionalProperties: false,
|
|
334
|
+
properties: Object.fromEntries(
|
|
335
|
+
INTERCEPT_SEVERITIES.map((severity) => [severity, { type: "string", enum: [...FAILURE_ACTIONS] }]),
|
|
336
|
+
),
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const INTERCEPTOR_JSON_SCHEMA = {
|
|
340
|
+
type: "object",
|
|
341
|
+
additionalProperties: false,
|
|
342
|
+
properties: {
|
|
343
|
+
enabled: { type: "boolean" },
|
|
344
|
+
severityActions: SEVERITY_ACTION_SCHEMA,
|
|
345
|
+
failurePolicy: FAILURE_POLICY_SCHEMA,
|
|
346
|
+
actionGuard: {
|
|
347
|
+
type: "object",
|
|
348
|
+
additionalProperties: false,
|
|
349
|
+
properties: {
|
|
350
|
+
enabled: { type: "boolean" },
|
|
351
|
+
enforce: { type: "boolean" },
|
|
352
|
+
autoApprove: { type: "array", items: { type: "string" } },
|
|
353
|
+
auditAllows: { type: "boolean" },
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
|
|
273
359
|
const PLUGIN_CONFIG_JSON_SCHEMA = {
|
|
274
360
|
type: "object",
|
|
275
361
|
additionalProperties: false,
|
|
@@ -282,6 +368,9 @@ const PLUGIN_CONFIG_JSON_SCHEMA = {
|
|
|
282
368
|
openclawAutoMemoryDedupe: { type: "boolean" },
|
|
283
369
|
openclawAutoMemoryNoveltyThreshold: { type: "number", minimum: 0.6, maximum: 0.99 },
|
|
284
370
|
openclawAutoMemoryMaxRecent: { type: "integer", minimum: 50, maximum: 1000 },
|
|
371
|
+
// #112: without this, `additionalProperties: false` declared the whole
|
|
372
|
+
// interceptor block invalid — mirror openclaw.plugin.json's configSchema.
|
|
373
|
+
interceptor: INTERCEPTOR_JSON_SCHEMA,
|
|
285
374
|
},
|
|
286
375
|
};
|
|
287
376
|
|
|
@@ -345,9 +434,92 @@ function normaliseConfig(raw: unknown): SCConfig {
|
|
|
345
434
|
config.openclawAutoMemoryMaxRecent = Math.floor(clamp(value.openclawAutoMemoryMaxRecent, 50, 1000));
|
|
346
435
|
}
|
|
347
436
|
|
|
437
|
+
// #112: the allowlist above predates the interceptor and silently dropped
|
|
438
|
+
// the entire nested `interceptor` object — `interceptor.enabled: false` was
|
|
439
|
+
// ignored and DEFAULT_INTERCEPTOR_CONFIG re-armed the before_tool_call gate.
|
|
440
|
+
// Validate-and-preserve every nested interceptor key the manifest schema
|
|
441
|
+
// accepts; invalid values are dropped individually, valid siblings survive.
|
|
442
|
+
const interceptor = normaliseInterceptorConfig(value.interceptor);
|
|
443
|
+
if (interceptor) config.interceptor = interceptor;
|
|
444
|
+
|
|
348
445
|
return config;
|
|
349
446
|
}
|
|
350
447
|
|
|
448
|
+
function normaliseSeverityMap<A extends string>(
|
|
449
|
+
raw: unknown,
|
|
450
|
+
allowed: readonly A[],
|
|
451
|
+
): Partial<Record<InterceptSeverity, A>> | undefined {
|
|
452
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
453
|
+
const value = raw as Record<string, unknown>;
|
|
454
|
+
const out: Partial<Record<InterceptSeverity, A>> = {};
|
|
455
|
+
for (const severity of INTERCEPT_SEVERITIES) {
|
|
456
|
+
const entry = value[severity];
|
|
457
|
+
if (typeof entry === "string" && (allowed as readonly string[]).includes(entry)) {
|
|
458
|
+
out[severity] = entry as A;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function normaliseInterceptorConfig(raw: unknown): InterceptorUserConfig | undefined {
|
|
465
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
466
|
+
const value = raw as Record<string, unknown>;
|
|
467
|
+
const out: InterceptorUserConfig = {};
|
|
468
|
+
|
|
469
|
+
if (typeof value.enabled === "boolean") out.enabled = value.enabled;
|
|
470
|
+
|
|
471
|
+
const severityActions = normaliseSeverityMap(value.severityActions, INTERCEPT_ACTIONS);
|
|
472
|
+
if (severityActions) out.severityActions = severityActions;
|
|
473
|
+
|
|
474
|
+
const failurePolicy = normaliseSeverityMap(value.failurePolicy, FAILURE_ACTIONS);
|
|
475
|
+
if (failurePolicy) out.failurePolicy = failurePolicy;
|
|
476
|
+
|
|
477
|
+
if (value.actionGuard && typeof value.actionGuard === "object" && !Array.isArray(value.actionGuard)) {
|
|
478
|
+
const rawGuard = value.actionGuard as Record<string, unknown>;
|
|
479
|
+
const guard: NonNullable<InterceptorUserConfig["actionGuard"]> = {};
|
|
480
|
+
if (typeof rawGuard.enabled === "boolean") guard.enabled = rawGuard.enabled;
|
|
481
|
+
if (typeof rawGuard.enforce === "boolean") guard.enforce = rawGuard.enforce;
|
|
482
|
+
if (typeof rawGuard.auditAllows === "boolean") guard.auditAllows = rawGuard.auditAllows;
|
|
483
|
+
if (Array.isArray(rawGuard.autoApprove) && rawGuard.autoApprove.every((entry) => typeof entry === "string")) {
|
|
484
|
+
guard.autoApprove = rawGuard.autoApprove as string[];
|
|
485
|
+
}
|
|
486
|
+
if (Object.keys(guard).length > 0) out.actionGuard = guard;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return out;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Merge two normalised configs (#112). Semantics:
|
|
494
|
+
* - Top-level scalar keys: override wins when present.
|
|
495
|
+
* - `interceptor` deep-merges PER KEY — per severity entry, per guard flag —
|
|
496
|
+
* so an override that sets one nested key does not wholesale-discard the
|
|
497
|
+
* base's other interceptor settings.
|
|
498
|
+
* - Explicit values, including `false`, always win over base values; absent
|
|
499
|
+
* keys fall through to the base.
|
|
500
|
+
* - Defaults are NOT applied here: DEFAULT_INTERCEPTOR_CONFIG only fills the
|
|
501
|
+
* remaining gaps in initInterceptor(), so it can never override an explicit
|
|
502
|
+
* user value.
|
|
503
|
+
*/
|
|
504
|
+
function mergeConfigs(base: SCConfig, override: SCConfig): SCConfig {
|
|
505
|
+
const merged: SCConfig = { ...base, ...override };
|
|
506
|
+
if (base.interceptor || override.interceptor) {
|
|
507
|
+
const b = base.interceptor ?? {};
|
|
508
|
+
const o = override.interceptor ?? {};
|
|
509
|
+
merged.interceptor = { ...b, ...o };
|
|
510
|
+
if (b.severityActions || o.severityActions) {
|
|
511
|
+
merged.interceptor.severityActions = { ...b.severityActions, ...o.severityActions };
|
|
512
|
+
}
|
|
513
|
+
if (b.failurePolicy || o.failurePolicy) {
|
|
514
|
+
merged.interceptor.failurePolicy = { ...b.failurePolicy, ...o.failurePolicy };
|
|
515
|
+
}
|
|
516
|
+
if (b.actionGuard || o.actionGuard) {
|
|
517
|
+
merged.interceptor.actionGuard = { ...b.actionGuard, ...o.actionGuard };
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return merged;
|
|
521
|
+
}
|
|
522
|
+
|
|
351
523
|
function extractPluginConfig(rootConfig: unknown): SCConfig {
|
|
352
524
|
if (!rootConfig || typeof rootConfig !== "object" || Array.isArray(rootConfig)) return {};
|
|
353
525
|
const entries = (rootConfig as {
|
|
@@ -372,10 +544,7 @@ function applyPluginConfigOverride(api: PluginApi): void {
|
|
|
372
544
|
: api.config;
|
|
373
545
|
const pluginConfig = extractPluginConfig(runtimeConfig);
|
|
374
546
|
if (Object.keys(pluginConfig).length === 0) return;
|
|
375
|
-
_configOverride = {
|
|
376
|
-
...(_configOverride ?? {}),
|
|
377
|
-
...pluginConfig,
|
|
378
|
-
};
|
|
547
|
+
_configOverride = mergeConfigs(_configOverride ?? {}, pluginConfig);
|
|
379
548
|
// Override changed — invalidate so loadConfig() re-merges with new override.
|
|
380
549
|
_config = null;
|
|
381
550
|
_lastShieldConfigRef = null;
|
|
@@ -385,10 +554,9 @@ async function loadConfig(): Promise<SCConfig> {
|
|
|
385
554
|
const shieldConfigRaw = await (await getRuntime()).loadShieldConfig();
|
|
386
555
|
if (_config && shieldConfigRaw === _lastShieldConfigRef) return _config;
|
|
387
556
|
_lastShieldConfigRef = shieldConfigRaw;
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
};
|
|
557
|
+
// Plugin config (openclaw.json) deep-merges over the shield config file —
|
|
558
|
+
// see mergeConfigs() for the per-key semantics.
|
|
559
|
+
_config = mergeConfigs(normaliseConfig(shieldConfigRaw), _configOverride ?? {});
|
|
392
560
|
return _config;
|
|
393
561
|
}
|
|
394
562
|
|
|
@@ -873,7 +1041,9 @@ export default {
|
|
|
873
1041
|
|
|
874
1042
|
try {
|
|
875
1043
|
const scConfig = await loadConfig();
|
|
876
|
-
|
|
1044
|
+
// Normalised user config (deep-partial); DEFAULT_INTERCEPTOR_CONFIG
|
|
1045
|
+
// fills the gaps below — defaults never override explicit values.
|
|
1046
|
+
const rawInterceptorConfig = scConfig.interceptor;
|
|
877
1047
|
const interceptorConfig: InterceptorConfig = {
|
|
878
1048
|
...DEFAULT_INTERCEPTOR_CONFIG,
|
|
879
1049
|
...(rawInterceptorConfig && typeof rawInterceptorConfig === 'object' ? {
|
|
@@ -948,7 +1118,7 @@ export default {
|
|
|
948
1118
|
const cloud = cfg.cloudApiKey ? "configured" : "not configured";
|
|
949
1119
|
// Resolve the Action Guard state the same way initInterceptor() does,
|
|
950
1120
|
// so the status line reflects what before_tool_call will actually do.
|
|
951
|
-
const rawInterceptor =
|
|
1121
|
+
const rawInterceptor = cfg.interceptor;
|
|
952
1122
|
const guardCfg = {
|
|
953
1123
|
...(DEFAULT_INTERCEPTOR_CONFIG.actionGuard ?? { enabled: true, enforce: true, autoApprove: [] }),
|
|
954
1124
|
...(rawInterceptor && typeof rawInterceptor === 'object' ? rawInterceptor.actionGuard ?? {} : {}),
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shieldcortex-realtime",
|
|
3
|
-
"version": "4.47.
|
|
3
|
+
"version": "4.47.14",
|
|
4
4
|
"name": "ShieldCortex Real-time Scanner",
|
|
5
5
|
"description": "Real-time defence scanning on LLM input, memory extraction on LLM output, and active tool call interception with approval gating.",
|
|
6
6
|
"kind": null,
|
package/package.json
CHANGED