@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.
@@ -273,31 +273,39 @@ export function formatActionGuardPrompt(toolName, v) {
273
273
  ].join('\n');
274
274
  }
275
275
  // --- Audit Logging (local JSONL) ---
276
- const AUDIT_DIR = join(homedir(), '.shieldcortex', 'audit');
276
+ /** Resolve per write so isolated tests can redirect every realtime audit path.
277
+ * The conversation hook already honours this variable; the interceptor did not,
278
+ * which caused otherwise-isolated Action Guard suites to append fabricated
279
+ * intercept rows to the host's real security audit. */
280
+ function auditDir() {
281
+ const override = process.env.SHIELDCORTEX_AUDIT_DIR;
282
+ return override?.trim() || join(homedir(), '.shieldcortex', 'audit');
283
+ }
277
284
  // Issue #95: an unwritable audit sink used to be swallowed by this bare catch —
278
285
  // entries silently dropped forever. Still best-effort (an audit failure must
279
286
  // never block the agent), but the FIRST failure now warns loudly with the sink
280
- // path and the error, and later failures keep a drop count for the breadcrumb.
287
+ // path and error. Suppress repeats so a broken disk does not flood gateway logs.
281
288
  let auditSinkFailures = 0;
282
- export function noteAuditSinkFailure(err) {
289
+ export function noteAuditSinkFailure(err, dir = auditDir()) {
283
290
  auditSinkFailures++;
284
291
  if (auditSinkFailures === 1) {
285
292
  const detail = err instanceof Error ? err.message : String(err);
286
- console.warn(`[shieldcortex] ⚠️ audit sink UNWRITABLE (${AUDIT_DIR}): ${detail} — audit entries are being DROPPED. ` +
293
+ console.warn(`[shieldcortex] ⚠️ audit sink UNWRITABLE (${dir}): ${detail} — audit entries are being DROPPED. ` +
287
294
  `Fix the directory permissions/disk; enforcement continues but leaves no trail until this is resolved.`);
288
295
  }
289
296
  }
290
297
  export function __resetAuditSinkFailuresForTest() { auditSinkFailures = 0; }
291
298
  function writeAuditEntry(entry) {
299
+ const dir = auditDir();
292
300
  try {
293
- mkdirSync(AUDIT_DIR, { recursive: true });
301
+ mkdirSync(dir, { recursive: true });
294
302
  const date = new Date().toISOString().slice(0, 10);
295
- const file = join(AUDIT_DIR, `realtime-${date}.jsonl`);
303
+ const file = join(dir, `realtime-${date}.jsonl`);
296
304
  appendFileSync(file, JSON.stringify(entry) + '\n');
297
305
  }
298
306
  catch (err) {
299
307
  // Best-effort — never block on audit failure, but never silent either (#95).
300
- noteAuditSinkFailure(err);
308
+ noteAuditSinkFailure(err, dir);
301
309
  }
302
310
  }
303
311
  // --- X-Ray Inline Guard ---
@@ -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
  }