@drakon-systems/shieldcortex-realtime 4.54.5 → 4.54.8

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
@@ -2776,6 +2776,18 @@ function truncateApprovalText(text, maxLength) {
2776
2776
  return normalized;
2777
2777
  return `${normalized.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
2778
2778
  }
2779
+ /** A held command can itself contain the credential that tripped the guard, and
2780
+ * an approval card is a chat surface. Same rule, same phrasing and same
2781
+ * trade as the Telegram card path (isSecretEgress / buildCardFields in
2782
+ * src/defence/iron-dome/openclaw-approval-channel.ts): the operator keeps the
2783
+ * tool, the signals and the severity, and loses exactly the text that must not
2784
+ * be forwarded. */
2785
+ const SECRET_EGRESS_PROMPT = /secret|credential/iu;
2786
+ const WITHHELD_COMMAND_TEXT = "(command withheld — contains credential material)";
2787
+ /** Prompt lines safe to forward on a secret-egress hold — label-only metadata.
2788
+ * `Reason:` (action guard) and `Content:` (memory write) are the two lines
2789
+ * that quote the payload, so anything not on this list is dropped. */
2790
+ const SAFE_APPROVAL_LINE = /^(?:Tool|Action|Risk|Signals|Threats):/iu;
2779
2791
  function buildTypedApprovalRequest(message) {
2780
2792
  const lines = message
2781
2793
  .split(/\r?\n/u)
@@ -2783,7 +2795,11 @@ function buildTypedApprovalRequest(message) {
2783
2795
  .filter(Boolean)
2784
2796
  .filter((line) => !/^\[(?:Approve|Deny)\]/i.test(line));
2785
2797
  const rawTitle = (lines[0] || "ShieldCortex approval required").replace(/^🛡️\s*/u, "");
2786
- const details = lines.slice(1).join(" | ") || rawTitle;
2798
+ const detailLines = lines.slice(1);
2799
+ const withholdPayload = SECRET_EGRESS_PROMPT.test(message);
2800
+ const details = (withholdPayload
2801
+ ? [WITHHELD_COMMAND_TEXT, ...detailLines.filter((line) => SAFE_APPROVAL_LINE.test(line))]
2802
+ : detailLines).join(" | ") || rawTitle;
2787
2803
  const riskText = message.toLowerCase();
2788
2804
  const severity = /\b(?:critical|catastrophic|auto[-_\s]?deny|exfil|rm\s+-rf)\b/u.test(riskText)
2789
2805
  ? "critical"
@@ -2794,20 +2810,36 @@ function buildTypedApprovalRequest(message) {
2794
2810
  title: truncateApprovalText(rawTitle, 80),
2795
2811
  description: truncateApprovalText(details, 256),
2796
2812
  severity,
2797
- timeoutMs: 120_000,
2813
+ // The host's own ceiling (MAX_PLUGIN_APPROVAL_TIMEOUT_MS), matching
2814
+ // CARD_TIMEOUT_MS on the Telegram card path. 120s was the old bridge's
2815
+ // number and it expired cards the operator was still walking back to.
2816
+ timeoutMs: 600_000,
2798
2817
  timeoutBehavior: "deny",
2799
2818
  allowedDecisions: ["allow-once", "deny"],
2800
2819
  };
2801
2820
  }
2802
- async function handleTypedBeforeToolCall(event, interceptor, logger, sessionId) {
2821
+ export const __buildTypedApprovalRequestForTest = buildTypedApprovalRequest;
2822
+ async function handleTypedBeforeToolCall(event, interceptor, logger, ctx) {
2823
+ const sessionId = resolveHookSessionId(event, ctx);
2824
+ // #310: a card is only worth minting when a human is there to tap it. Cron
2825
+ // and heartbeat runs are host-keyed automation with no operator attached, so
2826
+ // offering OpenClaw an approval request there recreates #112 exactly — the
2827
+ // turn waits out the timeout on a decision nobody can give. Withholding
2828
+ // `requireApproval` puts the interceptor on its unattended branch instead,
2829
+ // which denies on the failure policy immediately and loudly.
2830
+ const attended = !isTrustedAutomationSession(sessionId);
2803
2831
  try {
2804
2832
  await interceptor.handleToolCall({
2805
2833
  toolName: event.toolName,
2806
2834
  arguments: event.params ?? {},
2807
2835
  sessionId,
2808
- requireApproval: async (message) => {
2809
- throw new TypedApprovalRequest(message, buildTypedApprovalRequest(message));
2810
- },
2836
+ ...(attended
2837
+ ? {
2838
+ requireApproval: async (message) => {
2839
+ throw new TypedApprovalRequest(message, buildTypedApprovalRequest(message));
2840
+ },
2841
+ }
2842
+ : {}),
2811
2843
  });
2812
2844
  }
2813
2845
  catch (err) {
@@ -3100,7 +3132,10 @@ export default {
3100
3132
  // #233: the host supplies the session on the tool CONTEXT, not the
3101
3133
  // event. Without it a taint cannot be matched to the call it should
3102
3134
  // gate, so the escalation would silently never fire.
3103
- return handleTypedBeforeToolCall(event, interceptor, api.logger, ctx?.sessionId);
3135
+ // #310: the WHOLE context, not just `sessionId` — resolveHookSessionId
3136
+ // also reads `sessionKey`, which is where a cron/heartbeat run's key
3137
+ // actually arrives, and that key decides whether a card is minted.
3138
+ return handleTypedBeforeToolCall(event, interceptor, api.logger, ctx);
3104
3139
  }, { priority: 80, timeoutMs: 30_000 });
3105
3140
  _beforeToolCallRegistered = true;
3106
3141
  // NOTE: session_end is NOT registered here — it moved out of this guard
@@ -274,6 +274,22 @@ export function formatActionGuardPrompt(toolName, v) {
274
274
  '[Approve] [Deny]',
275
275
  ].join('\n');
276
276
  }
277
+ /**
278
+ * #310: the OpenClaw-native approval card is delivered by THROWING out of the
279
+ * injected `requireApproval` — the plugin's typed-hook bridge catches that
280
+ * throw and hands `{ requireApproval }` back to the host, which draws the card.
281
+ * So this particular rejection is control flow, not a failure, and the catch
282
+ * blocks below must let it pass straight through.
283
+ *
284
+ * Matched by NAME, not by class: the class lives in the plugin entrypoint
285
+ * (index.ts) and this file is deliberately free of a compile-time dependency on
286
+ * it, the same discipline as ToolGuardVerdictLike. Swallowing it as an approval
287
+ * error is exactly what turned every native card into a `failure_denied` the
288
+ * operator never saw.
289
+ */
290
+ function isTypedApprovalRequest(err) {
291
+ return err instanceof Error && err.name === 'TypedApprovalRequest';
292
+ }
277
293
  // --- Audit Logging (local JSONL) ---
278
294
  /** Resolve per write so isolated tests can redirect every realtime audit path.
279
295
  * The conversation hook already honours this variable; the interceptor did not,
@@ -912,6 +928,11 @@ export function createInterceptor(config, pipeline, options) {
912
928
  approved = await withApprovalDeadline(context.requireApproval(formatActionGuardPrompt(context.toolName, v)), brokered ? brokerApprovalTimeoutMs(v.severity) : 0);
913
929
  }
914
930
  catch (err) {
931
+ // #310: a minted approval card, not an error. Re-thrown untouched so the
932
+ // typed-hook bridge can turn it into the operator's card; auditing it
933
+ // here would write a denial for a decision nobody has made yet.
934
+ if (isTypedApprovalRequest(err))
935
+ throw err;
915
936
  if (brokered && err instanceof ApprovalTimeout) {
916
937
  // The asymmetric path. Silence is only ever a yes for something the
917
938
  // broker already pre-cleared — and that returned long before here — so
@@ -1080,6 +1101,10 @@ export function createInterceptor(config, pipeline, options) {
1080
1101
  approved = await context.requireApproval(message);
1081
1102
  }
1082
1103
  catch (err) {
1104
+ // #310: same bridge, same rule — the card request is not an approval
1105
+ // failure. Everything else below stays fail-closed.
1106
+ if (isTypedApprovalRequest(err))
1107
+ throw err;
1083
1108
  const failAction = config.failurePolicy[severity];
1084
1109
  log.warn(`[shieldcortex] ⚠️ requireApproval error: ${err instanceof Error ? err.message : err} — failure policy: ${failAction}`);
1085
1110
  const entry = {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.54.5",
3
+ "version": "4.54.8",
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,
@@ -121,7 +121,7 @@
121
121
  },
122
122
  "interceptor.conversation.posture": {
123
123
  "label": "Conversation Firewall",
124
- "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
+ "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 \u2014 OpenClaw refuses conversation hooks without that operator grant, and ShieldCortex will never set it for you.",
125
125
  "type": "string"
126
126
  },
127
127
  "interceptor.actionGuard.notify.enabled": {
@@ -132,7 +132,7 @@
132
132
  },
133
133
  "interceptor.actionGuard.notify.webhookUrl": {
134
134
  "label": "Notify Webhook URL",
135
- "description": "http(s) endpoint the notification is POSTed to. Conversation-firewall alerts carry no approve/deny affordance — there is nothing to approve.",
135
+ "description": "http(s) endpoint the notification is POSTed to. Conversation-firewall alerts carry no approve/deny affordance \u2014 there is nothing to approve.",
136
136
  "type": "string",
137
137
  "advanced": true
138
138
  },
@@ -156,7 +156,7 @@
156
156
  "properties": {
157
157
  "enabled": {
158
158
  "type": "boolean",
159
- "description": "Unused — plugin on/off is controlled by plugins.entries[id].enabled on the host, not this nested config value. See #115."
159
+ "description": "Unused \u2014 plugin on/off is controlled by plugins.entries[id].enabled on the host, not this nested config value. See #115."
160
160
  },
161
161
  "binaryPath": {
162
162
  "type": "string"
@@ -190,7 +190,7 @@
190
190
  "trustOwnerInput": {
191
191
  "type": "boolean",
192
192
  "default": true,
193
- "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
+ "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 \u2014 including another agent on a trusted channel \u2014 is data regardless of this setting."
194
194
  }
195
195
  }
196
196
  },
@@ -368,7 +368,7 @@
368
368
  "notify": {
369
369
  "type": "object",
370
370
  "additionalProperties": false,
371
- "description": "Operator-notify transport (#143), also used by the conversation firewall’s detection sink (#225). Off unless enabled is exactly true.",
371
+ "description": "Operator-notify transport (#143), also used by the conversation firewall\u2019s detection sink (#225). Off unless enabled is exactly true.",
372
372
  "properties": {
373
373
  "enabled": {
374
374
  "type": "boolean",
@@ -491,7 +491,7 @@
491
491
  "notify": {
492
492
  "type": "object",
493
493
  "additionalProperties": false,
494
- "description": "Operator-notify transport (#143), also used by the conversation firewall’s detection sink (#225). Off unless enabled is exactly true.",
494
+ "description": "Operator-notify transport (#143), also used by the conversation firewall\u2019s detection sink (#225). Off unless enabled is exactly true.",
495
495
  "properties": {
496
496
  "enabled": {
497
497
  "type": "boolean",
package/index.ts CHANGED
@@ -400,6 +400,10 @@ type AgentCtx = {
400
400
  type TypedBeforeToolCallEvent = {
401
401
  toolName: string;
402
402
  params?: Record<string, unknown>;
403
+ // Not supplied by today's OpenClaw (the session rides on the tool context —
404
+ // see #233), but resolveHookSessionId prefers the event when a host does
405
+ // send one, and every other hook's event carries it.
406
+ sessionId?: string;
403
407
  };
404
408
  type TypedBeforeToolCallResult = {
405
409
  block?: boolean;
@@ -3302,6 +3306,19 @@ function truncateApprovalText(text: string, maxLength: number): string {
3302
3306
  return `${normalized.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
3303
3307
  }
3304
3308
 
3309
+ /** A held command can itself contain the credential that tripped the guard, and
3310
+ * an approval card is a chat surface. Same rule, same phrasing and same
3311
+ * trade as the Telegram card path (isSecretEgress / buildCardFields in
3312
+ * src/defence/iron-dome/openclaw-approval-channel.ts): the operator keeps the
3313
+ * tool, the signals and the severity, and loses exactly the text that must not
3314
+ * be forwarded. */
3315
+ const SECRET_EGRESS_PROMPT = /secret|credential/iu;
3316
+ const WITHHELD_COMMAND_TEXT = "(command withheld — contains credential material)";
3317
+ /** Prompt lines safe to forward on a secret-egress hold — label-only metadata.
3318
+ * `Reason:` (action guard) and `Content:` (memory write) are the two lines
3319
+ * that quote the payload, so anything not on this list is dropped. */
3320
+ const SAFE_APPROVAL_LINE = /^(?:Tool|Action|Risk|Signals|Threats):/iu;
3321
+
3305
3322
  function buildTypedApprovalRequest(message: string): NonNullable<TypedBeforeToolCallResult["requireApproval"]> {
3306
3323
  const lines = message
3307
3324
  .split(/\r?\n/u)
@@ -3309,7 +3326,13 @@ function buildTypedApprovalRequest(message: string): NonNullable<TypedBeforeTool
3309
3326
  .filter(Boolean)
3310
3327
  .filter((line) => !/^\[(?:Approve|Deny)\]/i.test(line));
3311
3328
  const rawTitle = (lines[0] || "ShieldCortex approval required").replace(/^🛡️\s*/u, "");
3312
- const details = lines.slice(1).join(" | ") || rawTitle;
3329
+ const detailLines = lines.slice(1);
3330
+ const withholdPayload = SECRET_EGRESS_PROMPT.test(message);
3331
+ const details = (
3332
+ withholdPayload
3333
+ ? [WITHHELD_COMMAND_TEXT, ...detailLines.filter((line) => SAFE_APPROVAL_LINE.test(line))]
3334
+ : detailLines
3335
+ ).join(" | ") || rawTitle;
3313
3336
  const riskText = message.toLowerCase();
3314
3337
  const severity = /\b(?:critical|catastrophic|auto[-_\s]?deny|exfil|rm\s+-rf)\b/u.test(riskText)
3315
3338
  ? "critical"
@@ -3321,26 +3344,43 @@ function buildTypedApprovalRequest(message: string): NonNullable<TypedBeforeTool
3321
3344
  title: truncateApprovalText(rawTitle, 80),
3322
3345
  description: truncateApprovalText(details, 256),
3323
3346
  severity,
3324
- timeoutMs: 120_000,
3347
+ // The host's own ceiling (MAX_PLUGIN_APPROVAL_TIMEOUT_MS), matching
3348
+ // CARD_TIMEOUT_MS on the Telegram card path. 120s was the old bridge's
3349
+ // number and it expired cards the operator was still walking back to.
3350
+ timeoutMs: 600_000,
3325
3351
  timeoutBehavior: "deny",
3326
3352
  allowedDecisions: ["allow-once", "deny"],
3327
3353
  };
3328
3354
  }
3329
3355
 
3356
+ export const __buildTypedApprovalRequestForTest = buildTypedApprovalRequest;
3357
+
3330
3358
  async function handleTypedBeforeToolCall(
3331
3359
  event: TypedBeforeToolCallEvent,
3332
3360
  interceptor: ReturnType<typeof createInterceptor>,
3333
3361
  logger: PluginApi["logger"],
3334
- sessionId?: string,
3362
+ ctx?: AgentCtx,
3335
3363
  ): Promise<TypedBeforeToolCallResult | void> {
3364
+ const sessionId = resolveHookSessionId(event, ctx);
3365
+ // #310: a card is only worth minting when a human is there to tap it. Cron
3366
+ // and heartbeat runs are host-keyed automation with no operator attached, so
3367
+ // offering OpenClaw an approval request there recreates #112 exactly — the
3368
+ // turn waits out the timeout on a decision nobody can give. Withholding
3369
+ // `requireApproval` puts the interceptor on its unattended branch instead,
3370
+ // which denies on the failure policy immediately and loudly.
3371
+ const attended = !isTrustedAutomationSession(sessionId);
3336
3372
  try {
3337
3373
  await interceptor.handleToolCall({
3338
3374
  toolName: event.toolName,
3339
3375
  arguments: event.params ?? {},
3340
3376
  sessionId,
3341
- requireApproval: async (message: string) => {
3342
- throw new TypedApprovalRequest(message, buildTypedApprovalRequest(message));
3343
- },
3377
+ ...(attended
3378
+ ? {
3379
+ requireApproval: async (message: string) => {
3380
+ throw new TypedApprovalRequest(message, buildTypedApprovalRequest(message));
3381
+ },
3382
+ }
3383
+ : {}),
3344
3384
  });
3345
3385
  } catch (err) {
3346
3386
  if (err instanceof TypedApprovalRequest) {
@@ -3648,13 +3688,16 @@ export default {
3648
3688
  if (!interceptorDisabledInHostConfig) {
3649
3689
  // Typed before_tool_call hook: this is the OpenClaw agent-loop gate that
3650
3690
  // can block or require approval before the selected tool executes.
3651
- api.on('before_tool_call', async (event: TypedBeforeToolCallEvent, ctx?: { sessionId?: string }) => {
3691
+ api.on('before_tool_call', async (event: TypedBeforeToolCallEvent, ctx?: AgentCtx) => {
3652
3692
  const interceptor = await initInterceptor();
3653
3693
  if (!interceptor) return;
3654
3694
  // #233: the host supplies the session on the tool CONTEXT, not the
3655
3695
  // event. Without it a taint cannot be matched to the call it should
3656
3696
  // gate, so the escalation would silently never fire.
3657
- return handleTypedBeforeToolCall(event, interceptor, api.logger, ctx?.sessionId);
3697
+ // #310: the WHOLE context, not just `sessionId` — resolveHookSessionId
3698
+ // also reads `sessionKey`, which is where a cron/heartbeat run's key
3699
+ // actually arrives, and that key decides whether a card is minted.
3700
+ return handleTypedBeforeToolCall(event, interceptor, api.logger, ctx);
3658
3701
  }, { priority: 80, timeoutMs: 30_000 });
3659
3702
  _beforeToolCallRegistered = true;
3660
3703
  // NOTE: session_end is NOT registered here — it moved out of this guard
package/interceptor.ts CHANGED
@@ -547,6 +547,23 @@ export function formatActionGuardPrompt(toolName: string, v: ToolGuardVerdictLik
547
547
  ].join('\n');
548
548
  }
549
549
 
550
+ /**
551
+ * #310: the OpenClaw-native approval card is delivered by THROWING out of the
552
+ * injected `requireApproval` — the plugin's typed-hook bridge catches that
553
+ * throw and hands `{ requireApproval }` back to the host, which draws the card.
554
+ * So this particular rejection is control flow, not a failure, and the catch
555
+ * blocks below must let it pass straight through.
556
+ *
557
+ * Matched by NAME, not by class: the class lives in the plugin entrypoint
558
+ * (index.ts) and this file is deliberately free of a compile-time dependency on
559
+ * it, the same discipline as ToolGuardVerdictLike. Swallowing it as an approval
560
+ * error is exactly what turned every native card into a `failure_denied` the
561
+ * operator never saw.
562
+ */
563
+ function isTypedApprovalRequest(err: unknown): boolean {
564
+ return err instanceof Error && err.name === 'TypedApprovalRequest';
565
+ }
566
+
550
567
  // --- Audit Logging (local JSONL) ---
551
568
 
552
569
  /** Resolve per write so isolated tests can redirect every realtime audit path.
@@ -1297,6 +1314,10 @@ export function createInterceptor(
1297
1314
  brokered ? brokerApprovalTimeoutMs(v.severity) : 0,
1298
1315
  );
1299
1316
  } catch (err) {
1317
+ // #310: a minted approval card, not an error. Re-thrown untouched so the
1318
+ // typed-hook bridge can turn it into the operator's card; auditing it
1319
+ // here would write a denial for a decision nobody has made yet.
1320
+ if (isTypedApprovalRequest(err)) throw err;
1300
1321
  if (brokered && err instanceof ApprovalTimeout) {
1301
1322
  // The asymmetric path. Silence is only ever a yes for something the
1302
1323
  // broker already pre-cleared — and that returned long before here — so
@@ -1477,6 +1498,9 @@ export function createInterceptor(
1477
1498
  try {
1478
1499
  approved = await context.requireApproval(message);
1479
1500
  } catch (err) {
1501
+ // #310: same bridge, same rule — the card request is not an approval
1502
+ // failure. Everything else below stays fail-closed.
1503
+ if (isTypedApprovalRequest(err)) throw err;
1480
1504
  const failAction = config.failurePolicy[severity];
1481
1505
  log.warn(`[shieldcortex] ⚠️ requireApproval error: ${err instanceof Error ? err.message : err} — failure policy: ${failAction}`);
1482
1506
  const entry: InterceptAuditEntry = {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.54.5",
3
+ "version": "4.54.8",
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,
@@ -121,7 +121,7 @@
121
121
  },
122
122
  "interceptor.conversation.posture": {
123
123
  "label": "Conversation Firewall",
124
- "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
+ "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 \u2014 OpenClaw refuses conversation hooks without that operator grant, and ShieldCortex will never set it for you.",
125
125
  "type": "string"
126
126
  },
127
127
  "interceptor.actionGuard.notify.enabled": {
@@ -132,7 +132,7 @@
132
132
  },
133
133
  "interceptor.actionGuard.notify.webhookUrl": {
134
134
  "label": "Notify Webhook URL",
135
- "description": "http(s) endpoint the notification is POSTed to. Conversation-firewall alerts carry no approve/deny affordance — there is nothing to approve.",
135
+ "description": "http(s) endpoint the notification is POSTed to. Conversation-firewall alerts carry no approve/deny affordance \u2014 there is nothing to approve.",
136
136
  "type": "string",
137
137
  "advanced": true
138
138
  },
@@ -156,7 +156,7 @@
156
156
  "properties": {
157
157
  "enabled": {
158
158
  "type": "boolean",
159
- "description": "Unused — plugin on/off is controlled by plugins.entries[id].enabled on the host, not this nested config value. See #115."
159
+ "description": "Unused \u2014 plugin on/off is controlled by plugins.entries[id].enabled on the host, not this nested config value. See #115."
160
160
  },
161
161
  "binaryPath": {
162
162
  "type": "string"
@@ -190,7 +190,7 @@
190
190
  "trustOwnerInput": {
191
191
  "type": "boolean",
192
192
  "default": true,
193
- "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
+ "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 \u2014 including another agent on a trusted channel \u2014 is data regardless of this setting."
194
194
  }
195
195
  }
196
196
  },
@@ -368,7 +368,7 @@
368
368
  "notify": {
369
369
  "type": "object",
370
370
  "additionalProperties": false,
371
- "description": "Operator-notify transport (#143), also used by the conversation firewall’s detection sink (#225). Off unless enabled is exactly true.",
371
+ "description": "Operator-notify transport (#143), also used by the conversation firewall\u2019s detection sink (#225). Off unless enabled is exactly true.",
372
372
  "properties": {
373
373
  "enabled": {
374
374
  "type": "boolean",
@@ -491,7 +491,7 @@
491
491
  "notify": {
492
492
  "type": "object",
493
493
  "additionalProperties": false,
494
- "description": "Operator-notify transport (#143), also used by the conversation firewall’s detection sink (#225). Off unless enabled is exactly true.",
494
+ "description": "Operator-notify transport (#143), also used by the conversation firewall\u2019s detection sink (#225). Off unless enabled is exactly true.",
495
495
  "properties": {
496
496
  "enabled": {
497
497
  "type": "boolean",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakon-systems/shieldcortex-realtime",
3
- "version": "4.54.5",
3
+ "version": "4.54.8",
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",
@@ -24,10 +24,10 @@
24
24
  ],
25
25
  "scripts": {
26
26
  "pack:verify": "npm pack --dry-run",
27
- "prepublishOnly": "node -e \"if(!require('fs').existsSync('dist/index.js'))throw new Error('plugin dist/index.js missing — run `npm run build:ts` from the repo root before publishing')\""
27
+ "prepublishOnly": "node -e \"if(!require('fs').existsSync('dist/index.js'))throw new Error('plugin dist/index.js missing \u2014 run `npm run build:ts` from the repo root before publishing')\""
28
28
  },
29
29
  "peerDependencies": {
30
- "shieldcortex": ">=4.18.3 <5.0.0",
30
+ "shieldcortex": "^4.54.8",
31
31
  "openclaw": ">=2026.3.22"
32
32
  },
33
33
  "peerDependenciesMeta": {