@drakon-systems/shieldcortex-realtime 4.47.37 → 4.47.39

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/interceptor.ts CHANGED
@@ -510,19 +510,26 @@ export function formatActionGuardPrompt(toolName: string, v: ToolGuardVerdictLik
510
510
 
511
511
  // --- Audit Logging (local JSONL) ---
512
512
 
513
- const AUDIT_DIR = join(homedir(), '.shieldcortex', 'audit');
513
+ /** Resolve per write so isolated tests can redirect every realtime audit path.
514
+ * The conversation hook already honours this variable; the interceptor did not,
515
+ * which caused otherwise-isolated Action Guard suites to append fabricated
516
+ * intercept rows to the host's real security audit. */
517
+ function auditDir(): string {
518
+ const override = process.env.SHIELDCORTEX_AUDIT_DIR;
519
+ return override?.trim() || join(homedir(), '.shieldcortex', 'audit');
520
+ }
514
521
 
515
522
  // Issue #95: an unwritable audit sink used to be swallowed by this bare catch —
516
523
  // entries silently dropped forever. Still best-effort (an audit failure must
517
524
  // never block the agent), but the FIRST failure now warns loudly with the sink
518
- // path and the error, and later failures keep a drop count for the breadcrumb.
525
+ // path and error. Suppress repeats so a broken disk does not flood gateway logs.
519
526
  let auditSinkFailures = 0;
520
- export function noteAuditSinkFailure(err: unknown): void {
527
+ export function noteAuditSinkFailure(err: unknown, dir: string = auditDir()): void {
521
528
  auditSinkFailures++;
522
529
  if (auditSinkFailures === 1) {
523
530
  const detail = err instanceof Error ? err.message : String(err);
524
531
  console.warn(
525
- `[shieldcortex] ⚠️ audit sink UNWRITABLE (${AUDIT_DIR}): ${detail} — audit entries are being DROPPED. ` +
532
+ `[shieldcortex] ⚠️ audit sink UNWRITABLE (${dir}): ${detail} — audit entries are being DROPPED. ` +
526
533
  `Fix the directory permissions/disk; enforcement continues but leaves no trail until this is resolved.`,
527
534
  );
528
535
  }
@@ -530,14 +537,15 @@ export function noteAuditSinkFailure(err: unknown): void {
530
537
  export function __resetAuditSinkFailuresForTest(): void { auditSinkFailures = 0; }
531
538
 
532
539
  function writeAuditEntry(entry: InterceptAuditEntry): void {
540
+ const dir = auditDir();
533
541
  try {
534
- mkdirSync(AUDIT_DIR, { recursive: true });
542
+ mkdirSync(dir, { recursive: true });
535
543
  const date = new Date().toISOString().slice(0, 10);
536
- const file = join(AUDIT_DIR, `realtime-${date}.jsonl`);
544
+ const file = join(dir, `realtime-${date}.jsonl`);
537
545
  appendFileSync(file, JSON.stringify(entry) + '\n');
538
546
  } catch (err) {
539
547
  // Best-effort — never block on audit failure, but never silent either (#95).
540
- noteAuditSinkFailure(err);
548
+ noteAuditSinkFailure(err, dir);
541
549
  }
542
550
  }
543
551
 
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.47.37",
3
+ "version": "4.47.39",
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,
7
7
  "engines": {
8
8
  "openclaw": ">=2026.3.22",
9
- "recommended": ">=2026.4.23"
9
+ "recommended": ">=2026.4.23",
10
+ "conversationGate": ">=2026.5.12",
11
+ "conversationGateNote": "The before_agent_run gate (conversation firewall enforcement) first appears in OpenClaw 2026.5.9-beta.1 and first ships stable in 2026.5.12; 2026.5.7 has no such hook. Below that floor the hook registration is silently dropped by the host, so the plugin reports the conversation plane as observation-only rather than claiming enforcement. Everything else in this plugin still works at the `openclaw` floor above."
10
12
  },
11
13
  "enabledByDefault": false,
12
14
  "activation": {
@@ -14,6 +16,7 @@
14
16
  "hooks": [
15
17
  "llm_input",
16
18
  "llm_output",
19
+ "before_agent_run",
17
20
  "before_tool_call",
18
21
  "session_end"
19
22
  ],
@@ -102,6 +105,48 @@
102
105
  "description": "Write an audit entry when the guard evaluates a recognised (sensitive-tier) operation and allows it, so forensics can distinguish scanned-and-allowed from never-scanned. Benign allows are never audited.",
103
106
  "type": "boolean",
104
107
  "advanced": true
108
+ },
109
+ "interceptor.actionGuard.broker.enabled": {
110
+ "label": "AI Approval Broker",
111
+ "description": "Let a fast model judge dangerous-tier approvals before they reach you: it can deny outright when it sees injection, and release reversible, in-context, high-confidence actions without waiting. Never applies to catastrophic operations. Off by default.",
112
+ "type": "boolean",
113
+ "advanced": true
114
+ },
115
+ "interceptor.actionGuard.broker.allowPreClear": {
116
+ "label": "Allow Broker Pre-clear",
117
+ "description": "Off = every dangerous-tier action still waits for you; the broker can then only harden, never release.",
118
+ "type": "boolean",
119
+ "advanced": true
120
+ },
121
+ "interceptor.conversation.posture": {
122
+ "label": "Conversation Firewall",
123
+ "description": "What the conversation firewall does with a detection on the input path. off = do not scan; observe = scan, audit and alert the operator but never stop the turn (default); enforce = block the run via before_agent_run. Requires plugins.entries.shieldcortex-realtime.hooks.allowConversationAccess=true on this host — OpenClaw refuses conversation hooks without that operator grant, and ShieldCortex will never set it for you.",
124
+ "type": "string"
125
+ },
126
+ "interceptor.actionGuard.notify.enabled": {
127
+ "label": "Operator Notifications",
128
+ "description": "Reach a human when the guard holds an action, or when the conversation firewall detects a threat. Off by default.",
129
+ "type": "boolean",
130
+ "advanced": true
131
+ },
132
+ "interceptor.actionGuard.notify.webhookUrl": {
133
+ "label": "Notify Webhook URL",
134
+ "description": "http(s) endpoint the notification is POSTed to. Conversation-firewall alerts carry no approve/deny affordance — there is nothing to approve.",
135
+ "type": "string",
136
+ "advanced": true
137
+ },
138
+ "interceptor.actionGuard.notify.webhookSecret": {
139
+ "label": "Notify Webhook Secret",
140
+ "description": "HMAC-SHA256 key for X-ShieldCortex-Signature, so the receiver can reject spoofed POSTs.",
141
+ "type": "string",
142
+ "sensitive": true,
143
+ "advanced": true
144
+ },
145
+ "interceptor.actionGuard.notify.openclaw": {
146
+ "label": "Notify via OpenClaw",
147
+ "description": "Deliver through the gateway's own channel where the runtime provides that seam.",
148
+ "type": "boolean",
149
+ "advanced": true
105
150
  }
106
151
  },
107
152
  "configSchema": {
@@ -137,6 +182,17 @@
137
182
  "minimum": 50,
138
183
  "maximum": 1000
139
184
  },
185
+ "conversationTrust": {
186
+ "type": "object",
187
+ "additionalProperties": false,
188
+ "properties": {
189
+ "trustOwnerInput": {
190
+ "type": "boolean",
191
+ "default": true,
192
+ "description": "Default true: a message the host attributes to the gateway OWNER is an instruction, so a detection in it is audited and alerted but never taints the session or blocks the turn. Set false on a host where the owner routinely pastes untrusted content and you would rather have the caution than the quiet. Content from anyone else — including another agent on a trusted channel — is data regardless of this setting."
193
+ }
194
+ }
195
+ },
140
196
  "interceptor": {
141
197
  "type": "object",
142
198
  "additionalProperties": false,
@@ -225,6 +281,22 @@
225
281
  }
226
282
  }
227
283
  },
284
+ "conversation": {
285
+ "type": "object",
286
+ "additionalProperties": false,
287
+ "properties": {
288
+ "posture": {
289
+ "type": "string",
290
+ "enum": [
291
+ "off",
292
+ "observe",
293
+ "enforce"
294
+ ],
295
+ "default": "observe",
296
+ "description": "off = do not scan the conversation; observe = scan, audit and alert but never block (default); enforce = block the run on a dirty verdict via before_agent_run. Requires hooks.allowConversationAccess=true on this host."
297
+ }
298
+ }
299
+ },
228
300
  "actionGuard": {
229
301
  "type": "object",
230
302
  "additionalProperties": false,
@@ -247,10 +319,227 @@
247
319
  "auditAllows": {
248
320
  "type": "boolean",
249
321
  "default": true
322
+ },
323
+ "broker": {
324
+ "type": "object",
325
+ "additionalProperties": false,
326
+ "description": "AI-assisted approval broker (#143). Off unless enabled is exactly true. normaliseBrokerConfig in the main package still has the last word on every value here, so anything that slips past this schema is still range-checked and dropped before the broker sees it.",
327
+ "properties": {
328
+ "enabled": {
329
+ "type": "boolean",
330
+ "default": false
331
+ },
332
+ "allowPreClear": {
333
+ "type": "boolean",
334
+ "default": false
335
+ },
336
+ "preClearConfidence": {
337
+ "type": "number",
338
+ "minimum": 0.9,
339
+ "maximum": 1
340
+ },
341
+ "judgeTimeoutMs": {
342
+ "type": "number",
343
+ "minimum": 500,
344
+ "maximum": 60000
345
+ },
346
+ "approvalTimeoutMs": {
347
+ "type": "object",
348
+ "additionalProperties": false,
349
+ "properties": {
350
+ "sensitive": {
351
+ "type": "number",
352
+ "minimum": 1000,
353
+ "maximum": 3600000
354
+ },
355
+ "dangerous": {
356
+ "type": "number",
357
+ "minimum": 1000,
358
+ "maximum": 3600000
359
+ }
360
+ }
361
+ },
362
+ "model": {
363
+ "type": "string"
364
+ }
365
+ }
366
+ },
367
+ "notify": {
368
+ "type": "object",
369
+ "additionalProperties": false,
370
+ "description": "Operator-notify transport (#143), also used by the conversation firewall’s detection sink (#225). Off unless enabled is exactly true.",
371
+ "properties": {
372
+ "enabled": {
373
+ "type": "boolean",
374
+ "default": false
375
+ },
376
+ "webhookUrl": {
377
+ "type": "string"
378
+ },
379
+ "webhookSecret": {
380
+ "type": "string"
381
+ },
382
+ "openclaw": {
383
+ "type": "boolean",
384
+ "default": false
385
+ },
386
+ "timeoutMs": {
387
+ "type": "number",
388
+ "minimum": 500,
389
+ "maximum": 60000
390
+ }
391
+ }
392
+ },
393
+ "reviewedScripts": {
394
+ "type": "array",
395
+ "description": "Reviewed-script allowlist (#189). Each entry pins ONE script by absolute path and content hash; createReviewedScriptCheck has the last word on every field.",
396
+ "items": {
397
+ "type": "object",
398
+ "additionalProperties": false,
399
+ "properties": {
400
+ "path": {
401
+ "type": "string"
402
+ },
403
+ "sha256": {
404
+ "type": "string"
405
+ },
406
+ "note": {
407
+ "type": "string"
408
+ },
409
+ "addedAt": {
410
+ "type": "number"
411
+ }
412
+ },
413
+ "required": [
414
+ "path",
415
+ "sha256"
416
+ ]
417
+ }
250
418
  }
251
419
  }
252
420
  }
253
421
  }
422
+ },
423
+ "actionGuard": {
424
+ "type": "object",
425
+ "additionalProperties": false,
426
+ "properties": {
427
+ "enabled": {
428
+ "type": "boolean",
429
+ "default": true
430
+ },
431
+ "enforce": {
432
+ "type": "boolean",
433
+ "default": true
434
+ },
435
+ "autoApprove": {
436
+ "type": "array",
437
+ "items": {
438
+ "type": "string"
439
+ },
440
+ "default": []
441
+ },
442
+ "auditAllows": {
443
+ "type": "boolean",
444
+ "default": true
445
+ },
446
+ "broker": {
447
+ "type": "object",
448
+ "additionalProperties": false,
449
+ "description": "AI-assisted approval broker (#143). Off unless enabled is exactly true. normaliseBrokerConfig in the main package still has the last word on every value here, so anything that slips past this schema is still range-checked and dropped before the broker sees it.",
450
+ "properties": {
451
+ "enabled": {
452
+ "type": "boolean",
453
+ "default": false
454
+ },
455
+ "allowPreClear": {
456
+ "type": "boolean",
457
+ "default": false
458
+ },
459
+ "preClearConfidence": {
460
+ "type": "number",
461
+ "minimum": 0.9,
462
+ "maximum": 1
463
+ },
464
+ "judgeTimeoutMs": {
465
+ "type": "number",
466
+ "minimum": 500,
467
+ "maximum": 60000
468
+ },
469
+ "approvalTimeoutMs": {
470
+ "type": "object",
471
+ "additionalProperties": false,
472
+ "properties": {
473
+ "sensitive": {
474
+ "type": "number",
475
+ "minimum": 1000,
476
+ "maximum": 3600000
477
+ },
478
+ "dangerous": {
479
+ "type": "number",
480
+ "minimum": 1000,
481
+ "maximum": 3600000
482
+ }
483
+ }
484
+ },
485
+ "model": {
486
+ "type": "string"
487
+ }
488
+ }
489
+ },
490
+ "notify": {
491
+ "type": "object",
492
+ "additionalProperties": false,
493
+ "description": "Operator-notify transport (#143), also used by the conversation firewall’s detection sink (#225). Off unless enabled is exactly true.",
494
+ "properties": {
495
+ "enabled": {
496
+ "type": "boolean",
497
+ "default": false
498
+ },
499
+ "webhookUrl": {
500
+ "type": "string"
501
+ },
502
+ "webhookSecret": {
503
+ "type": "string"
504
+ },
505
+ "openclaw": {
506
+ "type": "boolean",
507
+ "default": false
508
+ },
509
+ "timeoutMs": {
510
+ "type": "number",
511
+ "minimum": 500,
512
+ "maximum": 60000
513
+ }
514
+ }
515
+ },
516
+ "reviewedScripts": {
517
+ "type": "array",
518
+ "description": "Reviewed-script allowlist (#189). Each entry pins ONE script by absolute path and content hash; createReviewedScriptCheck has the last word on every field.",
519
+ "items": {
520
+ "type": "object",
521
+ "additionalProperties": false,
522
+ "properties": {
523
+ "path": {
524
+ "type": "string"
525
+ },
526
+ "sha256": {
527
+ "type": "string"
528
+ },
529
+ "note": {
530
+ "type": "string"
531
+ },
532
+ "addedAt": {
533
+ "type": "number"
534
+ }
535
+ },
536
+ "required": [
537
+ "path",
538
+ "sha256"
539
+ ]
540
+ }
541
+ }
542
+ }
254
543
  }
255
544
  }
256
545
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakon-systems/shieldcortex-realtime",
3
- "version": "4.47.37",
3
+ "version": "4.47.39",
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",