@drakon-systems/shieldcortex-realtime 4.45.1 → 4.46.0

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 CHANGED
@@ -734,7 +734,7 @@ export default {
734
734
  enabled: rawInterceptorConfig.enabled ?? DEFAULT_INTERCEPTOR_CONFIG.enabled,
735
735
  severityActions: { ...DEFAULT_INTERCEPTOR_CONFIG.severityActions, ...rawInterceptorConfig.severityActions },
736
736
  failurePolicy: { ...DEFAULT_INTERCEPTOR_CONFIG.failurePolicy, ...rawInterceptorConfig.failurePolicy },
737
- actionGuard: { ...(DEFAULT_INTERCEPTOR_CONFIG.actionGuard ?? { enabled: true, enforce: false }), ...(rawInterceptorConfig.actionGuard ?? {}) },
737
+ actionGuard: { ...(DEFAULT_INTERCEPTOR_CONFIG.actionGuard ?? { enabled: true, enforce: true, autoApprove: [] }), ...(rawInterceptorConfig.actionGuard ?? {}) },
738
738
  } : {}),
739
739
  logger: { info: api.logger?.info ?? console.log, warn: api.logger?.warn ?? console.warn },
740
740
  };
@@ -27,10 +27,14 @@ const DEFAULT_CONFIG = {
27
27
  critical: 'deny',
28
28
  },
29
29
  // Action Guard on by default: catastrophic ops are blocked out of the box;
30
- // dangerous ops are surfaced (warn+audit) but allowed unless `enforce` is set.
30
+ // recognised-dangerous ops are ENFORCED by default (P1/WS1) — attended → prompt,
31
+ // unattended → fail closed on failurePolicy. Populate `autoApprove` per agent to
32
+ // pre-approve the dangerous ops it legitimately needs unattended; set
33
+ // `enforce:false` to opt back down to warn-and-allow.
31
34
  actionGuard: {
32
35
  enabled: true,
33
- enforce: false,
36
+ enforce: true,
37
+ autoApprove: [],
34
38
  },
35
39
  };
36
40
  export { WATCHED_TOOLS, CONTENT_FIELDS, DEFAULT_CONFIG };
@@ -227,7 +231,7 @@ export function createInterceptor(config, pipeline, options) {
227
231
  const rateLimiter = new RateLimiter(options?.maxPromptsPerMinute ?? 5);
228
232
  const log = config.logger ?? { info: console.log, warn: console.warn };
229
233
  const onAuditEntry = options?.onAuditEntry;
230
- const actionGuardCfg = config.actionGuard ?? { enabled: true, enforce: false };
234
+ const actionGuardCfg = config.actionGuard ?? { enabled: true, enforce: true, autoApprove: [] };
231
235
  const evaluateToolCall = options?.evaluateToolCall;
232
236
  function emitAudit(entry) {
233
237
  writeAuditEntry(entry);
@@ -272,16 +276,39 @@ export function createInterceptor(config, pipeline, options) {
272
276
  log.warn(`[shieldcortex] action-guard BLOCKED ${context.toolName}: ${v.reason} [${v.signals.join(", ")}]`);
273
277
  throw new Error(`ShieldCortex: tool call blocked — ${v.reason}`);
274
278
  }
275
- // require_approval — warn-only by default (never nags), prompt when enforcing.
279
+ // Per-agent autoApprove allowlist: a recognised-dangerous op whose family,
280
+ // action, or signal is pre-approved passes without gating. This is the escape
281
+ // hatch that lets enforce-by-default coexist with unattended agents doing
282
+ // legitimate dangerous work. It NEVER applies to catastrophic ops — those
283
+ // hard-block above, before this branch is reached.
284
+ const autoApprove = actionGuardCfg.autoApprove ?? [];
285
+ if (autoApprove.length > 0) {
286
+ const hay = [v.family, v.action, ...v.signals].map(s => String(s).toLowerCase());
287
+ const matched = autoApprove.some(a => {
288
+ const n = a.toLowerCase();
289
+ return hay.some(h => h === n || h.includes(n));
290
+ });
291
+ if (matched) {
292
+ emitAudit({ ...base, action: 'require_approval', outcome: 'approved' });
293
+ return;
294
+ }
295
+ }
296
+ // require_approval — ENFORCED by default (P1/WS1). `enforce:false` opts back
297
+ // down to warn-and-allow (advisory) for operators who want the old behaviour.
276
298
  if (!actionGuardCfg.enforce) {
277
299
  log.warn(`[shieldcortex] ⚠️ Action Guard: ${context.toolName} — ${v.reason}`);
278
300
  emitAudit({ ...base, action: 'warn', outcome: 'warned' });
279
301
  return;
280
302
  }
281
303
  if (typeof context.requireApproval !== 'function') {
304
+ // Unattended (no approver, e.g. cron/heartbeat): fail closed on the failure
305
+ // policy. High-severity dangerous defaults to deny — surfaced loudly to the
306
+ // gateway log so an operator sees it, because a silent no-op is exactly the
307
+ // failure mode we are eliminating.
282
308
  const failAction = config.failurePolicy[severity];
283
309
  emitAudit({ ...base, action: 'require_approval', outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed' });
284
310
  if (failAction === 'deny') {
311
+ log.warn(`[shieldcortex] action-guard DENIED (unattended, no approver) ${context.toolName}: ${v.reason} [${v.signals.join(", ")}]`);
285
312
  throw new Error(`ShieldCortex: tool call blocked — ${v.reason} (no approver, failure policy: deny)`);
286
313
  }
287
314
  return;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.45.1",
3
+ "version": "4.46.0",
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
@@ -880,7 +880,7 @@ export default {
880
880
  enabled: rawInterceptorConfig.enabled ?? DEFAULT_INTERCEPTOR_CONFIG.enabled,
881
881
  severityActions: { ...DEFAULT_INTERCEPTOR_CONFIG.severityActions, ...rawInterceptorConfig.severityActions },
882
882
  failurePolicy: { ...DEFAULT_INTERCEPTOR_CONFIG.failurePolicy, ...rawInterceptorConfig.failurePolicy },
883
- actionGuard: { ...(DEFAULT_INTERCEPTOR_CONFIG.actionGuard ?? { enabled: true, enforce: false }), ...(rawInterceptorConfig.actionGuard ?? {}) },
883
+ actionGuard: { ...(DEFAULT_INTERCEPTOR_CONFIG.actionGuard ?? { enabled: true, enforce: true, autoApprove: [] }), ...(rawInterceptorConfig.actionGuard ?? {}) },
884
884
  } : {}),
885
885
  logger: { info: api.logger?.info ?? console.log, warn: (api.logger as any)?.warn ?? console.warn },
886
886
  };
package/interceptor.ts CHANGED
@@ -10,14 +10,22 @@ export type FailureAction = 'allow' | 'deny';
10
10
  /**
11
11
  * Action Guard config — gates what the agent DOES (shell/file/network/git),
12
12
  * not just what it remembers. Catastrophic operations (rm -rf /, fork bombs,
13
- * disk wipes, secret exfil) are always blocked when `enabled`; recognised
14
- * dangerous ops require approval when `enforce` is set, otherwise they are
15
- * surfaced (warn + audit) but allowed through — so the guard never nags on
16
- * routine work by default.
13
+ * disk wipes, secret exfil) are always blocked when `enabled`.
14
+ *
15
+ * Recognised-dangerous ops (`rm <path>`, `sudo`, force-push, external egress,
16
+ * touching secret paths) are ENFORCED by default (P1/WS1): with an approver in
17
+ * the loop they prompt; unattended (no approver) they fail closed on the failure
18
+ * policy. `enforce:false` opts back down to warn-and-allow (advisory).
19
+ *
20
+ * `autoApprove` is the per-agent escape hatch: a dangerous op whose family,
21
+ * action, or signal matches an entry passes without gating — so enforce-by-default
22
+ * does not break unattended agents doing legitimate dangerous work. It NEVER
23
+ * relaxes catastrophic ops (those hard-block regardless).
17
24
  */
18
25
  export interface ActionGuardConfig {
19
26
  enabled: boolean;
20
27
  enforce: boolean;
28
+ autoApprove?: string[];
21
29
  }
22
30
 
23
31
  /** Structural shape of a Tool Action Guard verdict (kept local to avoid a
@@ -91,10 +99,14 @@ const DEFAULT_CONFIG: InterceptorConfig = {
91
99
  critical: 'deny',
92
100
  },
93
101
  // Action Guard on by default: catastrophic ops are blocked out of the box;
94
- // dangerous ops are surfaced (warn+audit) but allowed unless `enforce` is set.
102
+ // recognised-dangerous ops are ENFORCED by default (P1/WS1) — attended → prompt,
103
+ // unattended → fail closed on failurePolicy. Populate `autoApprove` per agent to
104
+ // pre-approve the dangerous ops it legitimately needs unattended; set
105
+ // `enforce:false` to opt back down to warn-and-allow.
95
106
  actionGuard: {
96
107
  enabled: true,
97
- enforce: false,
108
+ enforce: true,
109
+ autoApprove: [],
98
110
  },
99
111
  };
100
112
 
@@ -372,7 +384,7 @@ export function createInterceptor(
372
384
  const rateLimiter = new RateLimiter(options?.maxPromptsPerMinute ?? 5);
373
385
  const log = config.logger ?? { info: console.log, warn: console.warn };
374
386
  const onAuditEntry = options?.onAuditEntry;
375
- const actionGuardCfg: ActionGuardConfig = config.actionGuard ?? { enabled: true, enforce: false };
387
+ const actionGuardCfg: ActionGuardConfig = config.actionGuard ?? { enabled: true, enforce: true, autoApprove: [] };
376
388
  const evaluateToolCall = options?.evaluateToolCall;
377
389
 
378
390
  function emitAudit(entry: InterceptAuditEntry): void {
@@ -421,7 +433,26 @@ export function createInterceptor(
421
433
  throw new Error(`ShieldCortex: tool call blocked — ${v.reason}`);
422
434
  }
423
435
 
424
- // require_approval — warn-only by default (never nags), prompt when enforcing.
436
+ // Per-agent autoApprove allowlist: a recognised-dangerous op whose family,
437
+ // action, or signal is pre-approved passes without gating. This is the escape
438
+ // hatch that lets enforce-by-default coexist with unattended agents doing
439
+ // legitimate dangerous work. It NEVER applies to catastrophic ops — those
440
+ // hard-block above, before this branch is reached.
441
+ const autoApprove = actionGuardCfg.autoApprove ?? [];
442
+ if (autoApprove.length > 0) {
443
+ const hay = [v.family, v.action, ...v.signals].map(s => String(s).toLowerCase());
444
+ const matched = autoApprove.some(a => {
445
+ const n = a.toLowerCase();
446
+ return hay.some(h => h === n || h.includes(n));
447
+ });
448
+ if (matched) {
449
+ emitAudit({ ...base, action: 'require_approval', outcome: 'approved' });
450
+ return;
451
+ }
452
+ }
453
+
454
+ // require_approval — ENFORCED by default (P1/WS1). `enforce:false` opts back
455
+ // down to warn-and-allow (advisory) for operators who want the old behaviour.
425
456
  if (!actionGuardCfg.enforce) {
426
457
  log.warn(`[shieldcortex] ⚠️ Action Guard: ${context.toolName} — ${v.reason}`);
427
458
  emitAudit({ ...base, action: 'warn', outcome: 'warned' });
@@ -429,9 +460,14 @@ export function createInterceptor(
429
460
  }
430
461
 
431
462
  if (typeof context.requireApproval !== 'function') {
463
+ // Unattended (no approver, e.g. cron/heartbeat): fail closed on the failure
464
+ // policy. High-severity dangerous defaults to deny — surfaced loudly to the
465
+ // gateway log so an operator sees it, because a silent no-op is exactly the
466
+ // failure mode we are eliminating.
432
467
  const failAction = config.failurePolicy[severity];
433
468
  emitAudit({ ...base, action: 'require_approval', outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed' });
434
469
  if (failAction === 'deny') {
470
+ log.warn(`[shieldcortex] action-guard DENIED (unattended, no approver) ${context.toolName}: ${v.reason} [${v.signals.join(", ")}]`);
435
471
  throw new Error(`ShieldCortex: tool call blocked — ${v.reason} (no approver, failure policy: deny)`);
436
472
  }
437
473
  return;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.45.1",
3
+ "version": "4.46.0",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakon-systems/shieldcortex-realtime",
3
- "version": "4.45.1",
3
+ "version": "4.46.0",
4
4
  "description": "OpenClaw plugin for ShieldCortex real-time defence scanning and optional memory extraction.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",