@drakon-systems/shieldcortex-realtime 4.43.0 → 4.44.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/README.md CHANGED
@@ -29,6 +29,7 @@ The defensive root `openclaw.plugin.json` is kept for one release on the main pa
29
29
  |------|--------|
30
30
  | `llm_input` | Scans prompts and history through the ShieldCortex defence pipeline. Threats are logged to audit and can forward to ShieldCortex Cloud. |
31
31
  | `llm_output` | Extracts high-signal memories from assistant replies and writes them into ShieldCortex with novelty filtering and dedupe. |
32
+ | `before_tool_call` | Runs the Action Guard before tools execute. Catastrophic shell/file/network/git actions are blocked; dangerous actions warn by default or request approval when enforcement is enabled. |
32
33
 
33
34
  The plugin is intentionally fire-and-forget: it should not stall the OpenClaw turn loop if ShieldCortex is unavailable.
34
35
 
@@ -82,6 +83,9 @@ Example:
82
83
  "entries": {
83
84
  "shieldcortex-realtime": {
84
85
  "enabled": true,
86
+ "hooks": {
87
+ "allowConversationAccess": true
88
+ },
85
89
  "config": {
86
90
  "binaryPath": "/usr/local/bin/shieldcortex",
87
91
  "openclawAutoMemory": true,
package/dist/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * ShieldCortex Real-time Scanning Plugin for OpenClaw v2026.3.22+
3
3
  *
4
- * Uses explicit capability registration (registerHook + registerCommand)
5
- * for llm_input/llm_output scanning and optional memory extraction.
4
+ * Uses typed OpenClaw plugin hooks (`api.on`) for llm_input/llm_output
5
+ * scanning and before_tool_call interception. `api.registerHook` registers
6
+ * internal HOOK-style automation and does not participate in the agent-loop
7
+ * block/approval semantics ShieldCortex needs.
6
8
  * All scanning operations are fire-and-forget.
7
9
  */
8
10
  import { createHash } from "node:crypto";
@@ -640,6 +642,63 @@ function handleLlmOutput(event, ctx) {
640
642
  }
641
643
  })();
642
644
  }
645
+ class TypedApprovalRequest extends Error {
646
+ request;
647
+ constructor(message, request) {
648
+ super(message);
649
+ this.name = "TypedApprovalRequest";
650
+ this.request = request;
651
+ }
652
+ }
653
+ function truncateApprovalText(text, maxLength) {
654
+ const normalized = text.replace(/\s+/g, " ").trim();
655
+ if (normalized.length <= maxLength)
656
+ return normalized;
657
+ return `${normalized.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
658
+ }
659
+ function buildTypedApprovalRequest(message) {
660
+ const lines = message
661
+ .split(/\r?\n/u)
662
+ .map((line) => line.trim())
663
+ .filter(Boolean)
664
+ .filter((line) => !/^\[(?:Approve|Deny)\]/i.test(line));
665
+ const rawTitle = (lines[0] || "ShieldCortex approval required").replace(/^🛡️\s*/u, "");
666
+ const details = lines.slice(1).join(" | ") || rawTitle;
667
+ const riskText = message.toLowerCase();
668
+ const severity = /\b(?:critical|catastrophic|auto[-_\s]?deny|exfil|rm\s+-rf)\b/u.test(riskText)
669
+ ? "critical"
670
+ : /\b(?:high|dangerous|sensitive|risk|intercepted)\b/u.test(riskText)
671
+ ? "warning"
672
+ : "info";
673
+ return {
674
+ title: truncateApprovalText(rawTitle, 80),
675
+ description: truncateApprovalText(details, 256),
676
+ severity,
677
+ timeoutMs: 120_000,
678
+ timeoutBehavior: "deny",
679
+ allowedDecisions: ["allow-once", "deny"],
680
+ };
681
+ }
682
+ async function handleTypedBeforeToolCall(event, interceptor, logger) {
683
+ try {
684
+ await interceptor.handleToolCall({
685
+ toolName: event.toolName,
686
+ arguments: event.params ?? {},
687
+ requireApproval: async (message) => {
688
+ throw new TypedApprovalRequest(message, buildTypedApprovalRequest(message));
689
+ },
690
+ });
691
+ }
692
+ catch (err) {
693
+ if (err instanceof TypedApprovalRequest) {
694
+ return { requireApproval: err.request };
695
+ }
696
+ if (err instanceof Error && err.message.startsWith("ShieldCortex:")) {
697
+ return { block: true, blockReason: err.message };
698
+ }
699
+ logger?.warn?.(`[shieldcortex] before_tool_call error (allowing tool call): ${err instanceof Error ? err.message : err}`);
700
+ }
701
+ }
643
702
  // ==================== PLUGIN EXPORT ====================
644
703
  export default {
645
704
  id: PLUGIN_ID,
@@ -713,44 +772,23 @@ export default {
713
772
  return null;
714
773
  }
715
774
  }
716
- // Register before_tool_call with lazy-init wrapper
717
- api.registerHook('before_tool_call', async (context) => {
775
+ // Typed before_tool_call hook: this is the OpenClaw agent-loop gate that
776
+ // can block or require approval before the selected tool executes.
777
+ api.on('before_tool_call', async (event) => {
718
778
  const interceptor = await initInterceptor();
719
779
  if (!interceptor)
720
780
  return;
721
- try {
722
- await interceptor.handleToolCall(context);
723
- }
724
- catch (err) {
725
- // Intentional blocks from the interceptor (ShieldCortex: ...) should propagate
726
- if (err instanceof Error && err.message.startsWith('ShieldCortex:'))
727
- throw err;
728
- // Unexpected errors (DB crash, etc.) — log and allow the tool call through
729
- api.logger?.warn?.(`[shieldcortex] Interceptor error (allowing tool call): ${err instanceof Error ? err.message : err}`);
730
- }
731
- }, {
732
- name: 'shieldcortex-intercept-tool',
733
- description: 'Active threat gating on tool calls',
734
- });
781
+ return handleTypedBeforeToolCall(event, interceptor, api.logger);
782
+ }, { priority: 80, timeoutMs: 30_000 });
735
783
  // Try to register session_end for cache cleanup
736
784
  try {
737
- api.registerHook('session_end', () => { interceptorReady?.resetSession(); }, {
738
- name: 'shieldcortex-session-cleanup',
739
- description: 'Clear interceptor deny cache on session end',
740
- });
785
+ api.on('session_end', () => { interceptorReady?.resetSession(); });
741
786
  }
742
787
  catch {
743
788
  // session_end may not be a supported hook — TTL safety net handles this
744
789
  }
745
- // Explicit capability registration (replaces legacy api.on)
746
- api.registerHook("llm_input", handleLlmInput, {
747
- name: "shieldcortex-scan-input",
748
- description: "Real-time threat scanning on LLM input",
749
- });
750
- api.registerHook("llm_output", handleLlmOutput, {
751
- name: "shieldcortex-scan-output",
752
- description: "Memory extraction from LLM output",
753
- });
790
+ api.on("llm_input", handleLlmInput, { timeoutMs: 30_000 });
791
+ api.on("llm_output", handleLlmOutput, { timeoutMs: 30_000 });
754
792
  // Register a lightweight status command so the plugin is not hook-only
755
793
  api.registerCommand({
756
794
  name: "shieldcortex-status",
@@ -266,6 +266,10 @@ export function createInterceptor(config, pipeline, options) {
266
266
  // Catastrophic / exfil — hard block, always enforced when the guard is enabled.
267
267
  if (v.decision === 'block') {
268
268
  emitAudit({ ...base, action: 'auto_deny', outcome: 'auto_denied' });
269
+ // Surface the block to the gateway log (journald). Blocks are recorded in
270
+ // the ShieldCortex audit jsonl, but were otherwise invisible to an operator
271
+ // tailing the gateway; a denial — especially a false positive — must be seen.
272
+ log.warn(`[shieldcortex] action-guard BLOCKED ${context.toolName}: ${v.reason} [${v.signals.join(", ")}]`);
269
273
  throw new Error(`ShieldCortex: tool call blocked — ${v.reason}`);
270
274
  }
271
275
  // require_approval — warn-only by default (never nags), prompt when enforcing.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.43.0",
3
+ "version": "4.44.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
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * ShieldCortex Real-time Scanning Plugin for OpenClaw v2026.3.22+
3
3
  *
4
- * Uses explicit capability registration (registerHook + registerCommand)
5
- * for llm_input/llm_output scanning and optional memory extraction.
4
+ * Uses typed OpenClaw plugin hooks (`api.on`) for llm_input/llm_output
5
+ * scanning and before_tool_call interception. `api.registerHook` registers
6
+ * internal HOOK-style automation and does not participate in the agent-loop
7
+ * block/approval semantics ShieldCortex needs.
6
8
  * All scanning operations are fire-and-forget.
7
9
  */
8
10
 
@@ -14,7 +16,7 @@ import { homedir } from "node:os";
14
16
  import { fileURLToPath, pathToFileURL } from "node:url";
15
17
 
16
18
  import { createInterceptor, DEFAULT_CONFIG as DEFAULT_INTERCEPTOR_CONFIG } from './interceptor.js';
17
- import type { InterceptorConfig, ToolCallContext } from './interceptor.js';
19
+ import type { InterceptorConfig } from './interceptor.js';
18
20
  import { syncInterceptEvent } from './intercept-ingest.js';
19
21
  import { cloudSync } from './cloud-sync.js';
20
22
 
@@ -190,9 +192,26 @@ type AgentCtx = {
190
192
  agentId?: string; sessionKey?: string; sessionId?: string;
191
193
  workspaceDir?: string; messageProvider?: string;
192
194
  };
195
+ type TypedBeforeToolCallEvent = {
196
+ toolName: string;
197
+ params?: Record<string, unknown>;
198
+ };
199
+ type TypedBeforeToolCallResult = {
200
+ block?: boolean;
201
+ blockReason?: string;
202
+ requireApproval?: {
203
+ title: string;
204
+ description: string;
205
+ severity?: "info" | "warning" | "critical";
206
+ timeoutMs?: number;
207
+ timeoutBehavior?: "allow" | "deny";
208
+ allowedDecisions?: Array<"allow-once" | "allow-always" | "deny">;
209
+ onResolution?: (decision: "allow-once" | "allow-always" | "deny" | "timeout" | "cancelled") => Promise<void> | void;
210
+ };
211
+ };
193
212
  type PluginApi = {
194
213
  id: string; name: string; logger: { info: (m: string) => void };
195
- on: (hook: string, handler: (...args: any[]) => any) => void;
214
+ on: (hook: string, handler: (...args: any[]) => any, opts?: Record<string, unknown>) => void;
196
215
  [k: string]: any;
197
216
  };
198
217
 
@@ -756,6 +775,73 @@ function handleLlmOutput(event: LlmOutputEvent, ctx: AgentCtx): void {
756
775
  })();
757
776
  }
758
777
 
778
+ class TypedApprovalRequest extends Error {
779
+ request: NonNullable<TypedBeforeToolCallResult["requireApproval"]>;
780
+
781
+ constructor(message: string, request: NonNullable<TypedBeforeToolCallResult["requireApproval"]>) {
782
+ super(message);
783
+ this.name = "TypedApprovalRequest";
784
+ this.request = request;
785
+ }
786
+ }
787
+
788
+ function truncateApprovalText(text: string, maxLength: number): string {
789
+ const normalized = text.replace(/\s+/g, " ").trim();
790
+ if (normalized.length <= maxLength) return normalized;
791
+ return `${normalized.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
792
+ }
793
+
794
+ function buildTypedApprovalRequest(message: string): NonNullable<TypedBeforeToolCallResult["requireApproval"]> {
795
+ const lines = message
796
+ .split(/\r?\n/u)
797
+ .map((line) => line.trim())
798
+ .filter(Boolean)
799
+ .filter((line) => !/^\[(?:Approve|Deny)\]/i.test(line));
800
+ const rawTitle = (lines[0] || "ShieldCortex approval required").replace(/^🛡️\s*/u, "");
801
+ const details = lines.slice(1).join(" | ") || rawTitle;
802
+ const riskText = message.toLowerCase();
803
+ const severity = /\b(?:critical|catastrophic|auto[-_\s]?deny|exfil|rm\s+-rf)\b/u.test(riskText)
804
+ ? "critical"
805
+ : /\b(?:high|dangerous|sensitive|risk|intercepted)\b/u.test(riskText)
806
+ ? "warning"
807
+ : "info";
808
+
809
+ return {
810
+ title: truncateApprovalText(rawTitle, 80),
811
+ description: truncateApprovalText(details, 256),
812
+ severity,
813
+ timeoutMs: 120_000,
814
+ timeoutBehavior: "deny",
815
+ allowedDecisions: ["allow-once", "deny"],
816
+ };
817
+ }
818
+
819
+ async function handleTypedBeforeToolCall(
820
+ event: TypedBeforeToolCallEvent,
821
+ interceptor: ReturnType<typeof createInterceptor>,
822
+ logger: PluginApi["logger"],
823
+ ): Promise<TypedBeforeToolCallResult | void> {
824
+ try {
825
+ await interceptor.handleToolCall({
826
+ toolName: event.toolName,
827
+ arguments: event.params ?? {},
828
+ requireApproval: async (message: string) => {
829
+ throw new TypedApprovalRequest(message, buildTypedApprovalRequest(message));
830
+ },
831
+ });
832
+ } catch (err) {
833
+ if (err instanceof TypedApprovalRequest) {
834
+ return { requireApproval: err.request };
835
+ }
836
+
837
+ if (err instanceof Error && err.message.startsWith("ShieldCortex:")) {
838
+ return { block: true, blockReason: err.message };
839
+ }
840
+
841
+ (logger as any)?.warn?.(`[shieldcortex] before_tool_call error (allowing tool call): ${err instanceof Error ? err.message : err}`);
842
+ }
843
+ }
844
+
759
845
  // ==================== PLUGIN EXPORT ====================
760
846
 
761
847
  export default {
@@ -833,42 +919,23 @@ export default {
833
919
  }
834
920
  }
835
921
 
836
- // Register before_tool_call with lazy-init wrapper
837
- api.registerHook('before_tool_call', async (context: ToolCallContext) => {
922
+ // Typed before_tool_call hook: this is the OpenClaw agent-loop gate that
923
+ // can block or require approval before the selected tool executes.
924
+ api.on('before_tool_call', async (event: TypedBeforeToolCallEvent) => {
838
925
  const interceptor = await initInterceptor();
839
926
  if (!interceptor) return;
840
- try {
841
- await interceptor.handleToolCall(context);
842
- } catch (err) {
843
- // Intentional blocks from the interceptor (ShieldCortex: ...) should propagate
844
- if (err instanceof Error && err.message.startsWith('ShieldCortex:')) throw err;
845
- // Unexpected errors (DB crash, etc.) — log and allow the tool call through
846
- (api.logger as any)?.warn?.(`[shieldcortex] Interceptor error (allowing tool call): ${err instanceof Error ? err.message : err}`);
847
- }
848
- }, {
849
- name: 'shieldcortex-intercept-tool',
850
- description: 'Active threat gating on tool calls',
851
- });
927
+ return handleTypedBeforeToolCall(event, interceptor, api.logger);
928
+ }, { priority: 80, timeoutMs: 30_000 });
852
929
 
853
930
  // Try to register session_end for cache cleanup
854
931
  try {
855
- api.registerHook('session_end', () => { interceptorReady?.resetSession(); }, {
856
- name: 'shieldcortex-session-cleanup',
857
- description: 'Clear interceptor deny cache on session end',
858
- });
932
+ api.on('session_end', () => { interceptorReady?.resetSession(); });
859
933
  } catch {
860
934
  // session_end may not be a supported hook — TTL safety net handles this
861
935
  }
862
936
 
863
- // Explicit capability registration (replaces legacy api.on)
864
- api.registerHook("llm_input", handleLlmInput, {
865
- name: "shieldcortex-scan-input",
866
- description: "Real-time threat scanning on LLM input",
867
- });
868
- api.registerHook("llm_output", handleLlmOutput, {
869
- name: "shieldcortex-scan-output",
870
- description: "Memory extraction from LLM output",
871
- });
937
+ api.on("llm_input", handleLlmInput, { timeoutMs: 30_000 });
938
+ api.on("llm_output", handleLlmOutput, { timeoutMs: 30_000 });
872
939
 
873
940
  // Register a lightweight status command so the plugin is not hook-only
874
941
  api.registerCommand({
package/interceptor.ts CHANGED
@@ -414,6 +414,10 @@ export function createInterceptor(
414
414
  // Catastrophic / exfil — hard block, always enforced when the guard is enabled.
415
415
  if (v.decision === 'block') {
416
416
  emitAudit({ ...base, action: 'auto_deny', outcome: 'auto_denied' });
417
+ // Surface the block to the gateway log (journald). Blocks are recorded in
418
+ // the ShieldCortex audit jsonl, but were otherwise invisible to an operator
419
+ // tailing the gateway; a denial — especially a false positive — must be seen.
420
+ log.warn(`[shieldcortex] action-guard BLOCKED ${context.toolName}: ${v.reason} [${v.signals.join(", ")}]`);
417
421
  throw new Error(`ShieldCortex: tool call blocked — ${v.reason}`);
418
422
  }
419
423
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.43.0",
3
+ "version": "4.44.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.43.0",
3
+ "version": "4.44.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",