@drakon-systems/shieldcortex-realtime 4.47.15 → 4.47.17
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/interceptor.js +54 -3
- package/dist/openclaw.plugin.json +1 -1
- package/interceptor.ts +68 -4
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/interceptor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { mkdirSync, appendFileSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
2
|
+
import { mkdirSync, appendFileSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import { join, isAbsolute, resolve as resolvePath } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
const WATCHED_TOOLS = ['remember', 'mcp__memory__remember'];
|
|
6
6
|
const CONTENT_FIELDS = {
|
|
@@ -339,6 +339,52 @@ function xrayMemoryGuard(content, title) {
|
|
|
339
339
|
const riskLevel = score >= 80 ? 'SAFE' : score >= 60 ? 'LOW' : score >= 40 ? 'MEDIUM' : score >= 20 ? 'HIGH' : 'CRITICAL';
|
|
340
340
|
return { allowed: score >= 60, findings, riskLevel };
|
|
341
341
|
}
|
|
342
|
+
// ── Script source resolution (issue #4) ─────────────────────────────────────
|
|
343
|
+
// `bash deploy.sh` used to bypass EVERY Action Guard rule, because the guard
|
|
344
|
+
// only ever scanned the command string and never opened the file it pointed at.
|
|
345
|
+
// The guard core stays pure (doctor/self-check drive it with synthetic commands
|
|
346
|
+
// whose paths do not exist); this is the fs-backed half, wired in here.
|
|
347
|
+
//
|
|
348
|
+
// Zeroth law — this must never hang, block or crash the host gateway:
|
|
349
|
+
// * `statSync` first: only a REGULAR file is read, so a FIFO/socket/device
|
|
350
|
+
// (a `read` on which could block forever) is skipped, not opened;
|
|
351
|
+
// * `/proc`, `/sys`, `/dev` are never touched;
|
|
352
|
+
// * anything over the size cap is refused (the guard then records it as
|
|
353
|
+
// `opaque-script-invocation` rather than pretending it was scanned);
|
|
354
|
+
// * every error returns `null`. Nothing escapes.
|
|
355
|
+
const MAX_SCRIPT_SOURCE_BYTES = 262_144; // 256KB — matches the guard core's cap
|
|
356
|
+
const UNREADABLE_PATH_PREFIX = /^\/(?:proc|sys|dev)\//;
|
|
357
|
+
export function createScriptSourceResolver(cwd) {
|
|
358
|
+
const base = cwd && typeof cwd === 'string' ? cwd : process.cwd();
|
|
359
|
+
return (scriptPath) => {
|
|
360
|
+
try {
|
|
361
|
+
if (!scriptPath || typeof scriptPath !== 'string')
|
|
362
|
+
return null;
|
|
363
|
+
const expanded = scriptPath.startsWith('~/') ? join(homedir(), scriptPath.slice(2)) : scriptPath;
|
|
364
|
+
const full = isAbsolute(expanded) ? expanded : resolvePath(base, expanded);
|
|
365
|
+
if (UNREADABLE_PATH_PREFIX.test(full))
|
|
366
|
+
return null;
|
|
367
|
+
const st = statSync(full);
|
|
368
|
+
if (!st.isFile() || st.size > MAX_SCRIPT_SOURCE_BYTES)
|
|
369
|
+
return null;
|
|
370
|
+
return readFileSync(full, 'utf8');
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
return null; // missing, unreadable, anything — stay silent, stay alive
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
/** The cwd a tool call runs in, if the gateway or the call itself names one. */
|
|
378
|
+
function toolCallCwd(context) {
|
|
379
|
+
if (typeof context.cwd === 'string' && context.cwd)
|
|
380
|
+
return context.cwd;
|
|
381
|
+
for (const k of ['cwd', 'workdir', 'working_directory', 'workingDirectory', 'directory']) {
|
|
382
|
+
const v = context.arguments?.[k];
|
|
383
|
+
if (typeof v === 'string' && v)
|
|
384
|
+
return v;
|
|
385
|
+
}
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
342
388
|
export function createInterceptor(config, pipeline, options) {
|
|
343
389
|
const denyCache = new DenyCache();
|
|
344
390
|
const rateLimiter = new RateLimiter(options?.maxPromptsPerMinute ?? 5);
|
|
@@ -421,7 +467,12 @@ export function createInterceptor(config, pipeline, options) {
|
|
|
421
467
|
}
|
|
422
468
|
let v;
|
|
423
469
|
try {
|
|
424
|
-
|
|
470
|
+
// 4th arg (issue #4): lets the pure guard core scan the CONTENTS of a
|
|
471
|
+
// script the command invokes (`bash deploy.sh`) through this fs-backed
|
|
472
|
+
// resolver. An evaluator that predates the seam simply ignores it.
|
|
473
|
+
v = evaluateToolCall(context.toolName, context.arguments || {}, undefined, {
|
|
474
|
+
resolveScriptSource: createScriptSourceResolver(toolCallCwd(context)),
|
|
475
|
+
});
|
|
425
476
|
}
|
|
426
477
|
catch (err) {
|
|
427
478
|
handleGuardUnavailable(context, `action-guard error: ${err instanceof Error ? err.message : err}`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shieldcortex-realtime",
|
|
3
|
-
"version": "4.47.
|
|
3
|
+
"version": "4.47.17",
|
|
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/interceptor.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { mkdirSync, appendFileSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
2
|
+
import { mkdirSync, appendFileSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import { join, isAbsolute, resolve as resolvePath } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
|
|
6
6
|
export type Severity = 'low' | 'medium' | 'high' | 'critical';
|
|
@@ -42,7 +42,19 @@ export interface ToolGuardVerdictLike {
|
|
|
42
42
|
reason: string;
|
|
43
43
|
signals: string[];
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
/** Optional 4th-parameter seam on the real evaluator (issue #4): the guard core
|
|
46
|
+
* stays pure/synchronous and asks the CALLER to resolve an invoked script's
|
|
47
|
+
* source, so `bash deploy.sh` is scanned by the same rules as the inline
|
|
48
|
+
* command. Structurally typed, like ToolGuardVerdictLike above. */
|
|
49
|
+
export interface ToolGuardEvaluatorOptions {
|
|
50
|
+
resolveScriptSource?: (scriptPath: string) => string | null;
|
|
51
|
+
}
|
|
52
|
+
export type ToolGuardEvaluator = (
|
|
53
|
+
toolName: string,
|
|
54
|
+
args: Record<string, unknown>,
|
|
55
|
+
config?: unknown,
|
|
56
|
+
options?: ToolGuardEvaluatorOptions,
|
|
57
|
+
) => ToolGuardVerdictLike;
|
|
46
58
|
|
|
47
59
|
export interface InterceptorConfig {
|
|
48
60
|
enabled: boolean;
|
|
@@ -56,6 +68,10 @@ export interface ToolCallContext {
|
|
|
56
68
|
toolName: string;
|
|
57
69
|
arguments: Record<string, unknown>;
|
|
58
70
|
requireApproval?: (message: string) => Promise<boolean>;
|
|
71
|
+
/** Working directory the tool call runs in, when the gateway supplies one —
|
|
72
|
+
* used to resolve a relative script path (issue #4). Falls back to the
|
|
73
|
+
* call's own `cwd` argument, then `process.cwd()`. */
|
|
74
|
+
cwd?: string;
|
|
59
75
|
}
|
|
60
76
|
|
|
61
77
|
export interface InterceptAuditEntry {
|
|
@@ -487,6 +503,49 @@ type PipelineRunner = (content: string, title: string, source: { type: string; i
|
|
|
487
503
|
auditId: number;
|
|
488
504
|
};
|
|
489
505
|
|
|
506
|
+
// ── Script source resolution (issue #4) ─────────────────────────────────────
|
|
507
|
+
// `bash deploy.sh` used to bypass EVERY Action Guard rule, because the guard
|
|
508
|
+
// only ever scanned the command string and never opened the file it pointed at.
|
|
509
|
+
// The guard core stays pure (doctor/self-check drive it with synthetic commands
|
|
510
|
+
// whose paths do not exist); this is the fs-backed half, wired in here.
|
|
511
|
+
//
|
|
512
|
+
// Zeroth law — this must never hang, block or crash the host gateway:
|
|
513
|
+
// * `statSync` first: only a REGULAR file is read, so a FIFO/socket/device
|
|
514
|
+
// (a `read` on which could block forever) is skipped, not opened;
|
|
515
|
+
// * `/proc`, `/sys`, `/dev` are never touched;
|
|
516
|
+
// * anything over the size cap is refused (the guard then records it as
|
|
517
|
+
// `opaque-script-invocation` rather than pretending it was scanned);
|
|
518
|
+
// * every error returns `null`. Nothing escapes.
|
|
519
|
+
const MAX_SCRIPT_SOURCE_BYTES = 262_144; // 256KB — matches the guard core's cap
|
|
520
|
+
const UNREADABLE_PATH_PREFIX = /^\/(?:proc|sys|dev)\//;
|
|
521
|
+
|
|
522
|
+
export function createScriptSourceResolver(cwd?: string): (scriptPath: string) => string | null {
|
|
523
|
+
const base = cwd && typeof cwd === 'string' ? cwd : process.cwd();
|
|
524
|
+
return (scriptPath: string): string | null => {
|
|
525
|
+
try {
|
|
526
|
+
if (!scriptPath || typeof scriptPath !== 'string') return null;
|
|
527
|
+
const expanded = scriptPath.startsWith('~/') ? join(homedir(), scriptPath.slice(2)) : scriptPath;
|
|
528
|
+
const full = isAbsolute(expanded) ? expanded : resolvePath(base, expanded);
|
|
529
|
+
if (UNREADABLE_PATH_PREFIX.test(full)) return null;
|
|
530
|
+
const st = statSync(full);
|
|
531
|
+
if (!st.isFile() || st.size > MAX_SCRIPT_SOURCE_BYTES) return null;
|
|
532
|
+
return readFileSync(full, 'utf8');
|
|
533
|
+
} catch {
|
|
534
|
+
return null; // missing, unreadable, anything — stay silent, stay alive
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** The cwd a tool call runs in, if the gateway or the call itself names one. */
|
|
540
|
+
function toolCallCwd(context: ToolCallContext): string | undefined {
|
|
541
|
+
if (typeof context.cwd === 'string' && context.cwd) return context.cwd;
|
|
542
|
+
for (const k of ['cwd', 'workdir', 'working_directory', 'workingDirectory', 'directory']) {
|
|
543
|
+
const v = context.arguments?.[k];
|
|
544
|
+
if (typeof v === 'string' && v) return v;
|
|
545
|
+
}
|
|
546
|
+
return undefined;
|
|
547
|
+
}
|
|
548
|
+
|
|
490
549
|
interface InterceptorOptions {
|
|
491
550
|
maxPromptsPerMinute?: number;
|
|
492
551
|
onAuditEntry?: (entry: InterceptAuditEntry) => void;
|
|
@@ -591,7 +650,12 @@ export function createInterceptor(
|
|
|
591
650
|
|
|
592
651
|
let v: ToolGuardVerdictLike;
|
|
593
652
|
try {
|
|
594
|
-
|
|
653
|
+
// 4th arg (issue #4): lets the pure guard core scan the CONTENTS of a
|
|
654
|
+
// script the command invokes (`bash deploy.sh`) through this fs-backed
|
|
655
|
+
// resolver. An evaluator that predates the seam simply ignores it.
|
|
656
|
+
v = evaluateToolCall(context.toolName, context.arguments || {}, undefined, {
|
|
657
|
+
resolveScriptSource: createScriptSourceResolver(toolCallCwd(context)),
|
|
658
|
+
});
|
|
595
659
|
} catch (err) {
|
|
596
660
|
handleGuardUnavailable(context, `action-guard error: ${err instanceof Error ? err.message : err}`);
|
|
597
661
|
return;
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shieldcortex-realtime",
|
|
3
|
-
"version": "4.47.
|
|
3
|
+
"version": "4.47.17",
|
|
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