@drakon-systems/shieldcortex-realtime 4.47.25 → 4.47.27
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.
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ShieldCortex — the OpenClaw gateway operator-notify channel (#143).
|
|
3
|
+
*
|
|
4
|
+
* Design: docs/design/2026-07-31-ai-approval-broker.md, acceptance criterion 6:
|
|
5
|
+
* "On OpenClaw the transport should use the gateway's own message capability."
|
|
6
|
+
*
|
|
7
|
+
* Same shape and same reasoning as broker-invoker.ts's `createGatewayInvoker`:
|
|
8
|
+
* the gateway does not expose a message-sending seam to plugins by any fixed
|
|
9
|
+
* contract today, so this defines the narrowest one that could satisfy the
|
|
10
|
+
* design and fails closed when it is absent —
|
|
11
|
+
*
|
|
12
|
+
* no `context.notifyOperator` → no invoker → `createGatewayNotifyChannel`
|
|
13
|
+
* returns null → `requestOperatorApproval`'s resolution order (see
|
|
14
|
+
* operator-notify.ts) simply has one fewer candidate and falls through to
|
|
15
|
+
* whatever else is configured, ultimately to the unchanged
|
|
16
|
+
* hash-in-terminal fallback.
|
|
17
|
+
*
|
|
18
|
+
* Which is exactly today's behaviour on every gateway build shipping now:
|
|
19
|
+
* nothing regresses, and wiring this up costs nothing on a host that hasn't
|
|
20
|
+
* implemented the seam yet.
|
|
21
|
+
*
|
|
22
|
+
* What this file deliberately does NOT contain: any client for Telegram,
|
|
23
|
+
* WhatsApp, or any other chat platform. That lives entirely on the gateway
|
|
24
|
+
* side of `notifyOperator`, wherever the host chooses to implement it — this
|
|
25
|
+
* plugin only ever hands over structured data and a rendered text fallback,
|
|
26
|
+
* mirroring the "no Telegram client hard-coded into the guard core"
|
|
27
|
+
* requirement from the design doc.
|
|
28
|
+
*
|
|
29
|
+
* Types are declared/rendered LOCALLY rather than imported from
|
|
30
|
+
* `shieldcortex/defence/iron-dome/operator-notify.js` — same reason
|
|
31
|
+
* `ToolGuardVerdictLike` is structural in interceptor.ts (see that file's
|
|
32
|
+
* header): this module is built by tsconfig.openclaw-plugin.json, whose
|
|
33
|
+
* `rootDir` is `plugins/openclaw`, and a cross-boundary import fails that
|
|
34
|
+
* build (TS6059). `NotifyChannelLike`/`OperatorNotificationLike` mirror
|
|
35
|
+
* `operator-notify.ts`'s real shapes structurally, and the real
|
|
36
|
+
* `requestOperatorApproval` accepts anything satisfying `NotifyChannel`
|
|
37
|
+
* regardless of which module declared the type.
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* Local, minimal text rendering — deliberately duplicated rather than
|
|
41
|
+
* imported (see the module header). Kept in sync BY HAND with
|
|
42
|
+
* `formatOperatorNotification` in operator-notify.ts, whose own test suite
|
|
43
|
+
* pins the fields that must appear (exact command, tripped signals, tier,
|
|
44
|
+
* judge verdict, both affordances on the same hash); this local copy is
|
|
45
|
+
* pinned by this file's own tests below the same way.
|
|
46
|
+
*/
|
|
47
|
+
function renderText(n) {
|
|
48
|
+
const lines = [
|
|
49
|
+
'🛡️ ShieldCortex — approval needed',
|
|
50
|
+
'',
|
|
51
|
+
`Tool: ${n.tool}`,
|
|
52
|
+
`Command: ${n.command}`,
|
|
53
|
+
`Tripped: ${n.signals.join(', ') || 'none'}`,
|
|
54
|
+
`Tier: ${n.severity}`,
|
|
55
|
+
`Reason: ${n.reason}`,
|
|
56
|
+
'',
|
|
57
|
+
];
|
|
58
|
+
if (n.judge) {
|
|
59
|
+
lines.push(`AI judge: ${n.judge.assessment} (confidence ${n.judge.confidence})` +
|
|
60
|
+
`${n.judge.inContext ? '' : ', out of context'}` +
|
|
61
|
+
`${n.judge.injectionSuspected ? ', INJECTION SUSPECTED' : ''}`);
|
|
62
|
+
if (n.judge.rationale)
|
|
63
|
+
lines.push(` "${n.judge.rationale}"`);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
lines.push('AI judge: no judge ran — this verdict is rules-only');
|
|
67
|
+
}
|
|
68
|
+
lines.push('');
|
|
69
|
+
lines.push(`[Approve] shieldcortex approve ${n.shortHash}`);
|
|
70
|
+
lines.push(`[Deny] shieldcortex deny ${n.shortHash}`);
|
|
71
|
+
return lines.join('\n').slice(0, 4_000);
|
|
72
|
+
}
|
|
73
|
+
function buildMessage(n) {
|
|
74
|
+
return {
|
|
75
|
+
text: renderText(n),
|
|
76
|
+
hash: n.hash,
|
|
77
|
+
shortHash: n.shortHash,
|
|
78
|
+
tool: n.tool,
|
|
79
|
+
command: n.command,
|
|
80
|
+
signals: n.signals,
|
|
81
|
+
severity: n.severity,
|
|
82
|
+
approveCommand: `shieldcortex approve ${n.shortHash}`,
|
|
83
|
+
denyCommand: `shieldcortex deny ${n.shortHash}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Read the seam's ack as narrowly as `coerceCompletion` reads a completion in
|
|
88
|
+
* broker-invoker.ts: a resolved call with no explicit `{ delivered: false }`
|
|
89
|
+
* is treated as accepted (some hosts will not have a richer ack shape at
|
|
90
|
+
* all), but an EXPLICIT `delivered: false` is honoured, and nothing beyond
|
|
91
|
+
* the boolean is ever read — a hostile or careless host echoing back
|
|
92
|
+
* `approved: true` gets nothing from this for it.
|
|
93
|
+
*/
|
|
94
|
+
function coerceAck(raw) {
|
|
95
|
+
if (raw && typeof raw === 'object' && raw.delivered === false) {
|
|
96
|
+
const reason = raw.reason;
|
|
97
|
+
return { delivered: false, reason: typeof reason === 'string' ? reason : 'gateway reported delivery failure' };
|
|
98
|
+
}
|
|
99
|
+
return { delivered: true };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Build a notify channel from the gateway's own message seam, or null when
|
|
103
|
+
* the gateway does not offer one. Null is not an error state — see the
|
|
104
|
+
* module doc.
|
|
105
|
+
*/
|
|
106
|
+
export function createGatewayNotifyChannel(context) {
|
|
107
|
+
if (!context || typeof context !== 'object')
|
|
108
|
+
return null;
|
|
109
|
+
const notifyOperator = context.notifyOperator;
|
|
110
|
+
if (typeof notifyOperator !== 'function')
|
|
111
|
+
return null;
|
|
112
|
+
return {
|
|
113
|
+
name: 'gateway',
|
|
114
|
+
async send(notification) {
|
|
115
|
+
try {
|
|
116
|
+
const raw = await notifyOperator(buildMessage(notification));
|
|
117
|
+
return coerceAck(raw);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
return { delivered: false, reason: err instanceof Error ? err.message : String(err) };
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shieldcortex-realtime",
|
|
3
|
-
"version": "4.47.
|
|
3
|
+
"version": "4.47.27",
|
|
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/openclaw.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shieldcortex-realtime",
|
|
3
|
-
"version": "4.47.
|
|
3
|
+
"version": "4.47.27",
|
|
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