@drakon-systems/shieldcortex-realtime 4.43.0 → 4.45.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 +4 -0
- package/dist/index.js +69 -31
- package/dist/interceptor.js +4 -0
- package/dist/openclaw.plugin.json +1 -1
- package/index.ts +98 -31
- package/interceptor.ts +4 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
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
|
|
5
|
-
*
|
|
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
|
-
//
|
|
717
|
-
|
|
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
|
-
|
|
722
|
-
|
|
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.
|
|
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
|
-
|
|
746
|
-
api.
|
|
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",
|
package/dist/interceptor.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "4.45.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
|
|
5
|
-
*
|
|
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
|
|
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
|
-
//
|
|
837
|
-
|
|
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
|
-
|
|
841
|
-
|
|
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.
|
|
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
|
-
|
|
864
|
-
api.
|
|
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
|
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shieldcortex-realtime",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.45.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