@drakon-systems/shieldcortex-realtime 4.47.39 → 4.47.40
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 +89 -13
- package/dist/openclaw.plugin.json +1 -1
- package/index.ts +99 -13
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -29,6 +29,7 @@ import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
|
29
29
|
import path from "node:path";
|
|
30
30
|
import { homedir, hostname } from "node:os";
|
|
31
31
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
32
|
+
import { createRequire } from "node:module";
|
|
32
33
|
import { readConversationAccess, describeRegisteredHooks } from './conversation-access.js';
|
|
33
34
|
import { createSessionTaintStore } from './session-taint.js';
|
|
34
35
|
import { classifyConversationOrigin } from './conversation-trust.js';
|
|
@@ -52,13 +53,66 @@ function addAncestorCandidates(candidates, startPath) {
|
|
|
52
53
|
current = path.dirname(current);
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Ask Node where the `shieldcortex` package actually is (#174).
|
|
58
|
+
*
|
|
59
|
+
* The plugin declares `shieldcortex` as a peer, so on ANY layout Node's own
|
|
60
|
+
* resolver can find it from here — no guessing at install prefixes. Resolving
|
|
61
|
+
* `shieldcortex/package.json` rather than the runtime file directly is
|
|
62
|
+
* deliberate: `./package.json` is the one subpath the main package's `exports`
|
|
63
|
+
* map always declares, whereas `hooks/openclaw/**` is in `files` but NOT in
|
|
64
|
+
* `exports`, so resolving it throws ERR_PACKAGE_PATH_NOT_EXPORTED.
|
|
65
|
+
*
|
|
66
|
+
* This is the strategy that fixes the reported `~/.local` host, and it works
|
|
67
|
+
* without widening the public `exports` surface.
|
|
68
|
+
*/
|
|
69
|
+
function addResolvedPeerCandidate(candidates, fromUrl, resolve = (spec, from) => createRequire(from).resolve(spec)) {
|
|
70
|
+
try {
|
|
71
|
+
addRuntimeCandidate(candidates, path.dirname(resolve("shieldcortex/package.json", fromUrl)));
|
|
72
|
+
}
|
|
73
|
+
catch { /* not resolvable from here — later strategies still apply */ }
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Every place the runtime might live, in the order we should try them.
|
|
77
|
+
*
|
|
78
|
+
* `home` is injected because Jest sandboxes SHIELDCORTEX_CONFIG_DIR but never
|
|
79
|
+
* HOME, so a test that did not inject it would probe the developer's real
|
|
80
|
+
* install and pass for the wrong reason.
|
|
81
|
+
*/
|
|
82
|
+
function collectRuntimeCandidates(home = homedir(), resolveFrom = import.meta.url) {
|
|
56
83
|
const candidates = new Set();
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
//
|
|
84
|
+
// 0. Operator escape hatch. `resolveOpenClawBinary` and the approval channel
|
|
85
|
+
// already honour an env override for the same class of "we guessed your
|
|
86
|
+
// install prefix wrong" problem; this list was the only copy without one,
|
|
87
|
+
// which is why every new prefix (bun, volta, asdf, ~/.local) has needed a
|
|
88
|
+
// code change. Accepts either the runtime file itself or a package root.
|
|
89
|
+
const envOverride = process.env.SHIELDCORTEX_RUNTIME_PATH?.trim();
|
|
90
|
+
if (envOverride) {
|
|
91
|
+
if (envOverride.endsWith(".mjs") && existsSync(envOverride)) {
|
|
92
|
+
candidates.add(pathToFileURL(envOverride).href);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
addRuntimeCandidate(candidates, envOverride);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// 1. Ask Node. Works on every layout including the ~/.local one this fixes.
|
|
99
|
+
addResolvedPeerCandidate(candidates, resolveFrom);
|
|
100
|
+
// 2. Relative path — the repo/source-tree layout, where ../../hooks/… is real.
|
|
101
|
+
// GUARDED, unlike before: on an installed layout this resolves to a
|
|
102
|
+
// non-existent scoped path (`@drakon-systems/hooks/…`), and because it was
|
|
103
|
+
// the only unguarded entry it became the SOLE list member and its
|
|
104
|
+
// ERR_MODULE_NOT_FOUND became the operator-visible failure — the exact
|
|
105
|
+
// message #174 reports. It is a real candidate in the source tree, so it
|
|
106
|
+
// is kept and screened rather than deleted.
|
|
107
|
+
const relative = fileURLToPath(new URL("../../hooks/openclaw/cortex-memory/runtime.mjs", resolveFrom));
|
|
108
|
+
if (existsSync(relative))
|
|
109
|
+
candidates.add(pathToFileURL(relative).href);
|
|
110
|
+
// 3. Config file override. Honours SHIELDCORTEX_CONFIG_DIR like the rest of
|
|
111
|
+
// the product — reading homedir() directly made this permanently blind on
|
|
112
|
+
// a host that relocates its config.
|
|
60
113
|
try {
|
|
61
|
-
const
|
|
114
|
+
const configDir = process.env.SHIELDCORTEX_CONFIG_DIR?.trim() || path.join(home, ".shieldcortex");
|
|
115
|
+
const cfgPath = path.join(configDir, "config.json");
|
|
62
116
|
if (existsSync(cfgPath)) {
|
|
63
117
|
const cfg = JSON.parse(readFileSync(cfgPath, "utf-8"));
|
|
64
118
|
if (cfg.installRoot)
|
|
@@ -66,10 +120,15 @@ function collectRuntimeCandidates() {
|
|
|
66
120
|
}
|
|
67
121
|
}
|
|
68
122
|
catch { /* no config */ }
|
|
69
|
-
//
|
|
70
|
-
addAncestorCandidates(candidates, path.dirname(fileURLToPath(
|
|
71
|
-
//
|
|
72
|
-
for (const binDir of [
|
|
123
|
+
// 4. Walk up from current file location
|
|
124
|
+
addAncestorCandidates(candidates, path.dirname(fileURLToPath(resolveFrom)));
|
|
125
|
+
// 5. Resolve via common bin symlink paths (no child_process needed)
|
|
126
|
+
for (const binDir of [
|
|
127
|
+
"/usr/local/bin",
|
|
128
|
+
"/opt/homebrew/bin",
|
|
129
|
+
path.join(home, ".npm-global", "bin"),
|
|
130
|
+
path.join(home, ".local", "bin"), // #174: pip-style / npm --prefix ~/.local
|
|
131
|
+
]) {
|
|
73
132
|
const binPath = path.join(binDir, "shieldcortex");
|
|
74
133
|
try {
|
|
75
134
|
if (existsSync(binPath))
|
|
@@ -77,18 +136,19 @@ function collectRuntimeCandidates() {
|
|
|
77
136
|
}
|
|
78
137
|
catch { /* broken symlink */ }
|
|
79
138
|
}
|
|
80
|
-
//
|
|
139
|
+
// 6. Common global install paths (covers npm root -g results without spawning npm)
|
|
81
140
|
for (const root of [
|
|
82
141
|
"/usr/lib/node_modules/shieldcortex",
|
|
83
142
|
"/usr/local/lib/node_modules/shieldcortex",
|
|
84
143
|
"/opt/homebrew/lib/node_modules/shieldcortex",
|
|
85
|
-
path.join(
|
|
86
|
-
path.join(
|
|
144
|
+
path.join(home, ".npm-global", "lib", "node_modules", "shieldcortex"),
|
|
145
|
+
path.join(home, ".local", "lib", "node_modules", "shieldcortex"), // #174
|
|
146
|
+
path.join(home, ".nvm", "versions", "node"), // nvm users
|
|
87
147
|
]) {
|
|
88
148
|
if (root.includes(".nvm")) {
|
|
89
149
|
// For nvm, check the current symlink
|
|
90
150
|
try {
|
|
91
|
-
const currentNode = path.join(
|
|
151
|
+
const currentNode = path.join(home, ".nvm", "current", "lib", "node_modules", "shieldcortex");
|
|
92
152
|
addRuntimeCandidate(candidates, currentNode);
|
|
93
153
|
}
|
|
94
154
|
catch { /* no nvm */ }
|
|
@@ -120,6 +180,16 @@ async function getRuntime() {
|
|
|
120
180
|
lastError = error;
|
|
121
181
|
}
|
|
122
182
|
}
|
|
183
|
+
// #174: with every candidate screened by existsSync, "none found" is a
|
|
184
|
+
// real outcome and must not render as `Tried: . Last error: unknown
|
|
185
|
+
// error`. Name the escape hatch instead — this message is the only thing
|
|
186
|
+
// an operator on an unusual install prefix has to go on.
|
|
187
|
+
if (tried.length === 0) {
|
|
188
|
+
throw new Error("Could not load OpenClaw runtime: the shieldcortex package was not found from the plugin, " +
|
|
189
|
+
"and no known install prefix contained hooks/openclaw/cortex-memory/runtime.mjs. " +
|
|
190
|
+
"Point at it explicitly with SHIELDCORTEX_RUNTIME_PATH=/path/to/shieldcortex " +
|
|
191
|
+
"(or to the runtime.mjs itself), or reinstall so `shieldcortex` resolves as a peer of the plugin.");
|
|
192
|
+
}
|
|
123
193
|
const detail = lastError instanceof Error ? lastError.message : String(lastError ?? "unknown error");
|
|
124
194
|
throw new Error(`Could not load OpenClaw runtime. Tried: ${tried.join(", ")}. Last error: ${detail}`);
|
|
125
195
|
})();
|
|
@@ -159,6 +229,12 @@ async function getDefenceModule() {
|
|
|
159
229
|
export function __getSessionTaintForTest() {
|
|
160
230
|
return sessionTaint;
|
|
161
231
|
}
|
|
232
|
+
/** Test seam for #174 runtime resolution: `home` and the resolving module URL
|
|
233
|
+
* are injectable because Jest sandboxes SHIELDCORTEX_CONFIG_DIR but never
|
|
234
|
+
* HOME, so an un-injected probe would read the developer's real install. */
|
|
235
|
+
export function __collectRuntimeCandidatesForTest(home, from) {
|
|
236
|
+
return collectRuntimeCandidates(home, from);
|
|
237
|
+
}
|
|
162
238
|
export function __setDefenceModuleForTest(mod) {
|
|
163
239
|
_defenceModOverride = mod;
|
|
164
240
|
_defenceModPromise = null;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shieldcortex-realtime",
|
|
3
|
-
"version": "4.47.
|
|
3
|
+
"version": "4.47.40",
|
|
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
|
@@ -30,6 +30,7 @@ import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
|
30
30
|
import path from "node:path";
|
|
31
31
|
import { homedir, hostname } from "node:os";
|
|
32
32
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
33
|
+
import { createRequire } from "node:module";
|
|
33
34
|
|
|
34
35
|
import { readConversationAccess, describeRegisteredHooks } from './conversation-access.js';
|
|
35
36
|
import { createSessionTaintStore } from './session-taint.js';
|
|
@@ -120,44 +121,110 @@ function addAncestorCandidates(candidates: Set<string>, startPath: string) {
|
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Ask Node where the `shieldcortex` package actually is (#174).
|
|
126
|
+
*
|
|
127
|
+
* The plugin declares `shieldcortex` as a peer, so on ANY layout Node's own
|
|
128
|
+
* resolver can find it from here — no guessing at install prefixes. Resolving
|
|
129
|
+
* `shieldcortex/package.json` rather than the runtime file directly is
|
|
130
|
+
* deliberate: `./package.json` is the one subpath the main package's `exports`
|
|
131
|
+
* map always declares, whereas `hooks/openclaw/**` is in `files` but NOT in
|
|
132
|
+
* `exports`, so resolving it throws ERR_PACKAGE_PATH_NOT_EXPORTED.
|
|
133
|
+
*
|
|
134
|
+
* This is the strategy that fixes the reported `~/.local` host, and it works
|
|
135
|
+
* without widening the public `exports` surface.
|
|
136
|
+
*/
|
|
137
|
+
function addResolvedPeerCandidate(
|
|
138
|
+
candidates: Set<string>,
|
|
139
|
+
fromUrl: string,
|
|
140
|
+
resolve: (spec: string, from: string) => string = (spec, from) => createRequire(from).resolve(spec),
|
|
141
|
+
): void {
|
|
142
|
+
try {
|
|
143
|
+
addRuntimeCandidate(candidates, path.dirname(resolve("shieldcortex/package.json", fromUrl)));
|
|
144
|
+
} catch { /* not resolvable from here — later strategies still apply */ }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Every place the runtime might live, in the order we should try them.
|
|
149
|
+
*
|
|
150
|
+
* `home` is injected because Jest sandboxes SHIELDCORTEX_CONFIG_DIR but never
|
|
151
|
+
* HOME, so a test that did not inject it would probe the developer's real
|
|
152
|
+
* install and pass for the wrong reason.
|
|
153
|
+
*/
|
|
154
|
+
function collectRuntimeCandidates(
|
|
155
|
+
home: string = homedir(),
|
|
156
|
+
resolveFrom: string = import.meta.url,
|
|
157
|
+
): string[] {
|
|
124
158
|
const candidates = new Set<string>();
|
|
125
159
|
|
|
126
|
-
//
|
|
127
|
-
|
|
160
|
+
// 0. Operator escape hatch. `resolveOpenClawBinary` and the approval channel
|
|
161
|
+
// already honour an env override for the same class of "we guessed your
|
|
162
|
+
// install prefix wrong" problem; this list was the only copy without one,
|
|
163
|
+
// which is why every new prefix (bun, volta, asdf, ~/.local) has needed a
|
|
164
|
+
// code change. Accepts either the runtime file itself or a package root.
|
|
165
|
+
const envOverride = process.env.SHIELDCORTEX_RUNTIME_PATH?.trim();
|
|
166
|
+
if (envOverride) {
|
|
167
|
+
if (envOverride.endsWith(".mjs") && existsSync(envOverride)) {
|
|
168
|
+
candidates.add(pathToFileURL(envOverride).href);
|
|
169
|
+
} else {
|
|
170
|
+
addRuntimeCandidate(candidates, envOverride);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 1. Ask Node. Works on every layout including the ~/.local one this fixes.
|
|
175
|
+
addResolvedPeerCandidate(candidates, resolveFrom);
|
|
176
|
+
|
|
177
|
+
// 2. Relative path — the repo/source-tree layout, where ../../hooks/… is real.
|
|
178
|
+
// GUARDED, unlike before: on an installed layout this resolves to a
|
|
179
|
+
// non-existent scoped path (`@drakon-systems/hooks/…`), and because it was
|
|
180
|
+
// the only unguarded entry it became the SOLE list member and its
|
|
181
|
+
// ERR_MODULE_NOT_FOUND became the operator-visible failure — the exact
|
|
182
|
+
// message #174 reports. It is a real candidate in the source tree, so it
|
|
183
|
+
// is kept and screened rather than deleted.
|
|
184
|
+
const relative = fileURLToPath(new URL("../../hooks/openclaw/cortex-memory/runtime.mjs", resolveFrom));
|
|
185
|
+
if (existsSync(relative)) candidates.add(pathToFileURL(relative).href);
|
|
128
186
|
|
|
129
|
-
//
|
|
187
|
+
// 3. Config file override. Honours SHIELDCORTEX_CONFIG_DIR like the rest of
|
|
188
|
+
// the product — reading homedir() directly made this permanently blind on
|
|
189
|
+
// a host that relocates its config.
|
|
130
190
|
try {
|
|
131
|
-
const
|
|
191
|
+
const configDir = process.env.SHIELDCORTEX_CONFIG_DIR?.trim() || path.join(home, ".shieldcortex");
|
|
192
|
+
const cfgPath = path.join(configDir, "config.json");
|
|
132
193
|
if (existsSync(cfgPath)) {
|
|
133
194
|
const cfg = JSON.parse(readFileSync(cfgPath, "utf-8"));
|
|
134
195
|
if (cfg.installRoot) addRuntimeCandidate(candidates, cfg.installRoot);
|
|
135
196
|
}
|
|
136
197
|
} catch { /* no config */ }
|
|
137
198
|
|
|
138
|
-
//
|
|
139
|
-
addAncestorCandidates(candidates, path.dirname(fileURLToPath(
|
|
199
|
+
// 4. Walk up from current file location
|
|
200
|
+
addAncestorCandidates(candidates, path.dirname(fileURLToPath(resolveFrom)));
|
|
140
201
|
|
|
141
|
-
//
|
|
142
|
-
for (const binDir of [
|
|
202
|
+
// 5. Resolve via common bin symlink paths (no child_process needed)
|
|
203
|
+
for (const binDir of [
|
|
204
|
+
"/usr/local/bin",
|
|
205
|
+
"/opt/homebrew/bin",
|
|
206
|
+
path.join(home, ".npm-global", "bin"),
|
|
207
|
+
path.join(home, ".local", "bin"), // #174: pip-style / npm --prefix ~/.local
|
|
208
|
+
]) {
|
|
143
209
|
const binPath = path.join(binDir, "shieldcortex");
|
|
144
210
|
try {
|
|
145
211
|
if (existsSync(binPath)) addAncestorCandidates(candidates, realpathSync(binPath));
|
|
146
212
|
} catch { /* broken symlink */ }
|
|
147
213
|
}
|
|
148
214
|
|
|
149
|
-
//
|
|
215
|
+
// 6. Common global install paths (covers npm root -g results without spawning npm)
|
|
150
216
|
for (const root of [
|
|
151
217
|
"/usr/lib/node_modules/shieldcortex",
|
|
152
218
|
"/usr/local/lib/node_modules/shieldcortex",
|
|
153
219
|
"/opt/homebrew/lib/node_modules/shieldcortex",
|
|
154
|
-
path.join(
|
|
155
|
-
path.join(
|
|
220
|
+
path.join(home, ".npm-global", "lib", "node_modules", "shieldcortex"),
|
|
221
|
+
path.join(home, ".local", "lib", "node_modules", "shieldcortex"), // #174
|
|
222
|
+
path.join(home, ".nvm", "versions", "node"), // nvm users
|
|
156
223
|
]) {
|
|
157
224
|
if (root.includes(".nvm")) {
|
|
158
225
|
// For nvm, check the current symlink
|
|
159
226
|
try {
|
|
160
|
-
const currentNode = path.join(
|
|
227
|
+
const currentNode = path.join(home, ".nvm", "current", "lib", "node_modules", "shieldcortex");
|
|
161
228
|
addRuntimeCandidate(candidates, currentNode);
|
|
162
229
|
} catch { /* no nvm */ }
|
|
163
230
|
} else {
|
|
@@ -190,6 +257,18 @@ async function getRuntime(): Promise<OpenClawRuntime> {
|
|
|
190
257
|
}
|
|
191
258
|
}
|
|
192
259
|
|
|
260
|
+
// #174: with every candidate screened by existsSync, "none found" is a
|
|
261
|
+
// real outcome and must not render as `Tried: . Last error: unknown
|
|
262
|
+
// error`. Name the escape hatch instead — this message is the only thing
|
|
263
|
+
// an operator on an unusual install prefix has to go on.
|
|
264
|
+
if (tried.length === 0) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
"Could not load OpenClaw runtime: the shieldcortex package was not found from the plugin, " +
|
|
267
|
+
"and no known install prefix contained hooks/openclaw/cortex-memory/runtime.mjs. " +
|
|
268
|
+
"Point at it explicitly with SHIELDCORTEX_RUNTIME_PATH=/path/to/shieldcortex " +
|
|
269
|
+
"(or to the runtime.mjs itself), or reinstall so `shieldcortex` resolves as a peer of the plugin.",
|
|
270
|
+
);
|
|
271
|
+
}
|
|
193
272
|
const detail = lastError instanceof Error ? lastError.message : String(lastError ?? "unknown error");
|
|
194
273
|
throw new Error(`Could not load OpenClaw runtime. Tried: ${tried.join(", ")}. Last error: ${detail}`);
|
|
195
274
|
})();
|
|
@@ -232,6 +311,13 @@ export function __getSessionTaintForTest(): typeof sessionTaint {
|
|
|
232
311
|
return sessionTaint;
|
|
233
312
|
}
|
|
234
313
|
|
|
314
|
+
/** Test seam for #174 runtime resolution: `home` and the resolving module URL
|
|
315
|
+
* are injectable because Jest sandboxes SHIELDCORTEX_CONFIG_DIR but never
|
|
316
|
+
* HOME, so an un-injected probe would read the developer's real install. */
|
|
317
|
+
export function __collectRuntimeCandidatesForTest(home?: string, from?: string): string[] {
|
|
318
|
+
return collectRuntimeCandidates(home, from);
|
|
319
|
+
}
|
|
320
|
+
|
|
235
321
|
export function __setDefenceModuleForTest(mod: DefenceModule | null | undefined): void {
|
|
236
322
|
_defenceModOverride = mod;
|
|
237
323
|
_defenceModPromise = 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.40",
|
|
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