@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.
@@ -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
- v = evaluateToolCall(context.toolName, context.arguments || {});
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.15",
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
- export type ToolGuardEvaluator = (toolName: string, args: Record<string, unknown>) => ToolGuardVerdictLike;
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
- v = evaluateToolCall(context.toolName, context.arguments || {});
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;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.47.15",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakon-systems/shieldcortex-realtime",
3
- "version": "4.47.15",
3
+ "version": "4.47.17",
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",