@drakon-systems/shieldcortex-realtime 4.47.34 → 4.47.35

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 CHANGED
@@ -274,6 +274,22 @@ const INTERCEPTOR_JSON_SCHEMA = {
274
274
  model: { type: "string" },
275
275
  },
276
276
  },
277
+ // #189. Each entry pins one script by absolute path + content hash;
278
+ // createReviewedScriptCheck has the last word on every field.
279
+ reviewedScripts: {
280
+ type: "array",
281
+ items: {
282
+ type: "object",
283
+ additionalProperties: false,
284
+ properties: {
285
+ path: { type: "string" },
286
+ sha256: { type: "string" },
287
+ note: { type: "string" },
288
+ addedAt: { type: "number" },
289
+ },
290
+ required: ["path", "sha256"],
291
+ },
292
+ },
277
293
  },
278
294
  },
279
295
  },
@@ -485,6 +501,11 @@ function normaliseActionGuardBlock(raw, dropped, pathPrefix) {
485
501
  if (rawGuard.broker && typeof rawGuard.broker === "object" && !Array.isArray(rawGuard.broker)) {
486
502
  guard.broker = rawGuard.broker;
487
503
  }
504
+ // #189: same passthrough discipline — createReviewedScriptCheck is the
505
+ // boundary that shape-validates each entry.
506
+ if (Array.isArray(rawGuard.reviewedScripts)) {
507
+ guard.reviewedScripts = [...rawGuard.reviewedScripts];
508
+ }
488
509
  return Object.keys(guard).length > 0 ? guard : undefined;
489
510
  }
490
511
  function normaliseInterceptorConfig(raw, dropped) {
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { mkdirSync, appendFileSync, readFileSync, statSync } from 'node:fs';
2
+ import { mkdirSync, appendFileSync, readFileSync, realpathSync, statSync } from 'node:fs';
3
3
  import { join, isAbsolute, resolve as resolvePath } from 'node:path';
4
4
  import { homedir } from 'node:os';
5
5
  import { createGatewayInvoker } from './broker-invoker.js';
@@ -390,6 +390,56 @@ export function createScriptSourceResolver(cwd) {
390
390
  }
391
391
  };
392
392
  }
393
+ // #189 reviewed-script allowlist — DUPLICATED from
394
+ // src/defence/iron-dome/reviewed-scripts.ts for the same build-boundary reason
395
+ // as the resolver above (TS6059), and held to it by the same parity test
396
+ // (src/__tests__/enforcement-surface-parity.test.ts). Same rails, same
397
+ // answers, or the drift test goes red.
398
+ const REVIEWED_MAX_ENTRIES = 200;
399
+ const REVIEWED_MAX_PATH_LENGTH = 1_024;
400
+ const REVIEWED_SHA256_RE = /^[0-9a-f]{64}$/;
401
+ export function createReviewedScriptCheck(rawEntries, cwd) {
402
+ if (!Array.isArray(rawEntries) || rawEntries.length === 0)
403
+ return () => false;
404
+ const byCanonical = new Map(); // canonical path → sha256
405
+ for (const item of rawEntries.slice(0, REVIEWED_MAX_ENTRIES)) {
406
+ if (typeof item !== 'object' || item === null || Array.isArray(item))
407
+ continue;
408
+ const rec = item;
409
+ if (typeof rec.path !== 'string' || typeof rec.sha256 !== 'string')
410
+ continue;
411
+ const path = rec.path.trim();
412
+ const sha256 = rec.sha256.trim().toLowerCase();
413
+ if (!path || path.length > REVIEWED_MAX_PATH_LENGTH || !isAbsolute(path))
414
+ continue;
415
+ if (!REVIEWED_SHA256_RE.test(sha256))
416
+ continue;
417
+ try {
418
+ byCanonical.set(realpathSync(path), sha256);
419
+ }
420
+ catch {
421
+ /* pinned file missing — entry cannot match anything */
422
+ }
423
+ }
424
+ if (byCanonical.size === 0)
425
+ return () => false;
426
+ const base = cwd && typeof cwd === 'string' ? cwd : process.cwd();
427
+ return (scriptPath, source) => {
428
+ try {
429
+ if (!scriptPath || typeof scriptPath !== 'string' || typeof source !== 'string')
430
+ return false;
431
+ const expanded = scriptPath.startsWith('~/') ? join(homedir(), scriptPath.slice(2)) : scriptPath;
432
+ const full = isAbsolute(expanded) ? expanded : resolvePath(base, expanded);
433
+ const expected = byCanonical.get(realpathSync(full));
434
+ if (!expected)
435
+ return false;
436
+ return createHash('sha256').update(source, 'utf8').digest('hex') === expected;
437
+ }
438
+ catch {
439
+ return false;
440
+ }
441
+ };
442
+ }
393
443
  /** Raised when the operator never answered the approval card (#143). Distinct
394
444
  * from a transport error, because the two have opposite handling: an error
395
445
  * routes to failurePolicy, a timeout routes to the broker's asymmetric rule. */
@@ -469,6 +519,10 @@ export function createInterceptor(config, pipeline, options) {
469
519
  anomalyScore: v.decision === 'block' ? 1 : v.decision === 'allow' ? 0.1 : 0.6,
470
520
  trustScore: 0, sensitivityLevel: 'INTERNAL', fragmentationScore: null, pipelineDurationMs: 0,
471
521
  preview: preview.slice(0, 200), ts: new Date().toISOString(),
522
+ // #192: the durable record keeps the evidence, not just the rule names.
523
+ ...(v.matches && v.matches.length > 0 ? { matches: v.matches } : {}),
524
+ // #189: an allow that leaned on the reviewed-script allowlist says so.
525
+ ...(v.reviewedScripts && v.reviewedScripts.length > 0 ? { reviewedScripts: v.reviewedScripts } : {}),
472
526
  };
473
527
  }
474
528
  // ── Approval broker (#143) ────────────────────────────────────────────────
@@ -628,6 +682,11 @@ export function createInterceptor(config, pipeline, options) {
628
682
  // resolver. An evaluator that predates the seam simply ignores it.
629
683
  v = evaluateToolCall(context.toolName, context.arguments || {}, undefined, {
630
684
  resolveScriptSource: createScriptSourceResolver(toolCallCwd(context)),
685
+ // #189: same allowlist, same predicate semantics as the hook surface —
686
+ // config is RAW here, validated inside createReviewedScriptCheck.
687
+ ...(actionGuardCfg.reviewedScripts && actionGuardCfg.reviewedScripts.length > 0
688
+ ? { isReviewedScript: createReviewedScriptCheck(actionGuardCfg.reviewedScripts, toolCallCwd(context)) }
689
+ : {}),
631
690
  });
632
691
  }
633
692
  catch (err) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.47.31",
3
+ "version": "4.47.35",
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,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "shieldcortex-realtime",
3
- "version": "4.47.34",
3
+ "version": "4.47.35",
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.34",
3
+ "version": "4.47.35",
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",