Skill: jarvis-drakon/shieldcortex (version 4.18.3)

--- name: shieldcortex description: > Persistent memory and security system for AI agents. Stores memories with semantic search, knowledge graphs, and decay. Scans agent inputs/outputs for prompt injection, credential leaks, and poisoning. Audits agent instruction files and MCP configs. Includes Cortex mistake-learning module (Pro tier). license: MIT-0 metadata: author: Drakon Systems version: 4.18.3 mcp-server: shieldcortex category: memory-and-security tags: [memory, security, knowledge-graph, mcp, iron-dome, openclaw-plugin, audit] source: https://github.com/Drakon-Systems-Ltd/ShieldCortex homepage: https://shieldcortex.ai npm: https://www.npmjs.com/package/shieldcortex verified_publisher: Drakon Systems Ltd publisher_github: https://github.com/Drakon-Systems-Ltd npm_audit: clean snyk: no-known-vulnerabilities downloads: 9700+ install: command: shieldcortex quickstart runtime: node minVersion: "18" note: > Run the installed `shieldcortex` binary directly. The quickstart command detects your environment and guides MCP server registration. All data stays local in ~/.shieldcortex/. No account or API key needed for local use. permissions: filesystem: readwrite network: optional credentials: optional justification: > Filesystem read: scans agent instruction files for prompt injection threats (same files the agent already reads). Filesystem write: stores memory DB and config in ~/.shieldcortex/. Network: off by default, only used when Cloud sync is explicitly enabled by the user. Credentials: optional Cloud API key for team sync (not required for local use). paths_read: - ~/.shieldcortex/ (own config and memory database) - ~/.claude/ (project memory files, MCP config) - ~/.openclaw/ (MCP config, extensions) - ~/.cursor/ (rules, memories, MCP config) - ~/.windsurf/ (memories, rules) - ~/.codex/ (MCP config) - $CWD/.claude/, $CWD/.cursor/ (project-level configs) - $CWD/.cursorrules, $CWD/.windsurfrules, $CWD/.clinerules - $CWD/CLAUDE.md, $CWD/copilot-instructions.md - $CWD/.aider.conf.yml, $CWD/.continue/config.json - $CWD/.env (env-scanner checks for leaked secrets — reads, never writes) paths_write: - ~/.shieldcortex/ (memory DB, config, cortex log, licence, audit cache) - ~/.openclaw/extensions/shieldcortex-realtime/ (OpenClaw plugin, when user opts in) - ~/.claude/mcp.json, ~/.cursor/mcp.json (MCP server registration, when user runs setup) network_endpoints: - https://api.shieldcortex.ai (Cloud sync, licence validation — only when Cloud is enabled by user) - http://localhost:3001 (local dashboard server — loopback only) - http://localhost:3030 (local worker health check — loopback only) env: - SHIELDCORTEX_CONFIG_DIR: Override config directory (default ~/.shieldcortex/) - SHIELDCORTEX_API_KEY: Cloud sync API key (team tier only, optional) - SHIELDCORTEX_LICENSE_TIER: Override licence tier (development use) - SHIELDCORTEX_SKIP_EMBEDDINGS: Disable embedding generation - SHIELDCORTEX_HOST: Override dashboard/API bind host - PORT: Override dashboard/API port --- # ShieldCortex — Persistent Memory & Security for AI Agents Memory system with built-in security. Gives agents persistent memory (semantic search, knowledge graphs, decay, contradiction detection) and protects it with a 6-layer defence pipeline (prompt injection, credential leaks, poisoning, privilege escalation, PII filtering, behavioural analysis). Skill threat patterns (tool injection, scope escalation, data exfiltration, persistence, supply-chain, agent manipulation, stealth instructions) now also block at memory-write time, not just on skill-file scans. ## Provenance & Trust | Signal | Value | |--------|-------| | **Publisher** | [Drakon Systems Ltd](https://github.com/Drakon-Systems-Ltd) (UK company) | | **Source code** | [github.com/Drakon-Systems-Ltd/ShieldCortex](https://github.com/Drakon-Systems-Ltd/ShieldCortex) — fully open, MIT-0 licence | | **npm package** | [npmjs.com/package/shieldcortex](https://www.npmjs.com/package/shieldcortex) — published via GitHub Actions CI | | **npm audit** | Clean — `npm audit` returns 0 vulnerabilities | | **Downloads** | 9,700+ total (April 2026) | | **CI/CD** | Automated: push to main → CI lint/test → version tag → npm publish | | **No postinstall scripts** | Package has no lifecycle scripts that auto-execute on install | | **Dependencies** | 3 runtime deps: `better-sqlite3`, `zod`, `hono`. No transitive network libs. | ## Safety & Scope This section explains every privileged operation the tool performs and why. - **User-initiated only.** Setup is a manual step the user runs in their terminal. Nothing auto-executes on install. The `quickstart` command asks before each action. - **No credentials required for local use.** Memory, scanning, and audit work fully offline. Cloud sync (team tier) requires a user-provided API key via `shieldcortex config --cloud-enable --cloud-api-key <key>`. - **File access is declared and scoped.** Security scans read agent config directories listed in the permissions block above — the same directories the agent itself already has access to. They do not traverse arbitrary directories. - **Writes are contained.** All data goes to `~/.shieldcortex/`. MCP config edits (`setup`, `copilot`, `codex` commands) modify specific JSON files and confirm before writing. - **Network is off by default.** No outbound connections unless Cloud sync is explicitly enabled by the user. The dashboard and worker bind to localhost only. - **Bundled source code.** The OpenClaw plugin and cortex-memory handler are shipped in the package for inspection before use. - **Lifecycle event handlers.** ShieldCortex registers lifecycle handlers that auto-extract important context from conversations. These are registered in `~/.claude/settings.json` during setup and can be removed at any time. They run locally, never phone home. - **Proactive recall.** The UserPromptSubmit handler queries local memory on each prompt (<100ms) and surfaces relevant context. Fully local, configurable: `shieldcortex config --proactive-recall false`. ## What it does NOT do - Does **not** read SSH keys, AWS credentials, GPG keys, or /etc/ files - Does **not** send data to external servers (unless Cloud sync is explicitly enabled) - Does **not** modify .bashrc, .zshrc, .profile, or shell configs - Does **not** use eval(), child_process.exec(), or dynamic code execution - Does **not** bypass, disable, or override any agent safety mechanisms - Does **not** auto-approve actions or skip verification prompts - Does **not** mine cryptocurrency, trade tokens, manage wallets, or initiate purchases - Does **not** make purchases, place orders, or move money on the user's behalf ## CLI Reference ### Getting Started ```bash shieldcortex quickstart # Detect integrations, guide setup shieldcortex setup # Register MCP server for current project shieldcortex doctor # Diagnose registration issues shieldcortex status # Show protection status shieldcortex uninstall # Remove from project ``` ### Memory ```bash # Memory is typically used via MCP server, not CLI directly. # The MCP server exposes: store, recall, search, forget, consolidate, graph. shieldcortex graph backfill # Build knowledge graph from stored memories shieldcortex stats # Memory statistics ``` ### Security Scanning ```bash shieldcortex scan "text" # Scan text through defence pipeline shieldcortex scan-skill path/to/SKILL.md # Scan one instruction file for threats shieldcortex scan-skills # Scan all discovered agent instruction files shieldcortex audit # Full security audit (memory, env, MCP configs, rules files) shieldcortex iron-dome status # Iron Dome behavioural protection status ``` ### Cortex — Mistake Learning (Pro) ```bash shieldcortex cortex capture --task "..." --mistake "..." --fix "..." # Log a mistake shieldcortex cortex preflight --task "deploy to production" # Pre-task check shieldcortex cortex review # Pattern analysis shieldcortex cortex list # View mistake log shieldcortex cortex stats # Category breakdown ``` ### Dashboard & Services ```bash shieldcortex dashboard # Open local web dashboard (localhost:3001) shieldcortex api # Start API server shieldcortex worker # Background sync + heartbeat worker shieldcortex service start|stop|status # Manage background service ``` ### Integrations ```bash shieldcortex openclaw setup # Set up OpenClaw realtime plugin shieldcortex copilot setup # Set up VS Code / Cursor MCP server shieldcortex codex setup # Set up Codex CLI MCP server shieldcortex config --openclaw-auto-memory true # Enable auto-memory in OpenClaw shieldcortex config --proactive-recall true|false # Enable/disable proactive recall ``` ### Cloud & Licensing ```bash shieldcortex config --cloud-enable --cloud-api-key <key> # Enable cloud sync shieldcortex cloud sync --full # Backfill memories + graph to cloud shieldcortex license activate sc_pro_... # Activate Pro/Team licence shieldcortex license status # Check licence tier ``` ### Maintenance ```bash shieldcortex update # Self-update (npm package + OpenClaw plugin + skill) ``` ## What Gets Scanned ### `scan-skills` discovers and scans: - SKILL.md, HOOK.md, handler.js (Claude Code / OpenClaw skills) - .cursorrules, .windsurfrules, .clinerules (editor rules) - CLAUDE.md, copilot-instructions.md (agent instructions) - .aider.conf.yml, .continue/config.json (tool configs) - Searches: ~/.claude/skills/, ~/.openclaw/skills/, ~/.openclaw/hooks/, project directories ### `audit` checks: - **Memory files** — ~/.claude/projects/, ~/.cursor/memories/, ~/.windsurf/memories/ - **Environment** — .env files for leaked credentials (read-only check, never writes) - **MCP configs** — ~/.claude/mcp.json, ~/.openclaw/mcp.json, ~/.cursor/mcp.json, project-level equivalents - **Rules files** — CLAUDE.md, .cursorrules, copilot-instructions.md for injection patterns ## What Gets Uploaded to Cloud Cloud sync is **Team tier only** and **off by default**. - **Uploaded when Cloud sync is enabled by the user:** selected memory records, related embeddings/metadata, and knowledge-graph entities/relationships required for sync. - **Not uploaded by default:** local agent configs, MCP configs, raw rules files, shell configs, SSH keys, secrets, `.env` contents, or arbitrary project files. - **Security scan results stay local** unless the user explicitly exports or syncs data through a Cloud-enabled workflow. - **No cloud traffic at all** occurs unless the user explicitly enables Cloud sync and provides a valid API key. ## Licence Tiers | Feature | Free | Pro | Team | |---------|------|-----|------| | Memory (store/recall/search/graph) | ✅ | ✅ | ✅ | | Proactive recall (auto-inject on prompts) | ✅ | ✅ | ✅ | | Defence pipeline (scan, Iron Dome) | ✅ | ✅ | ✅ | | Audit & scan-skills | ✅ | ✅ | ✅ | | Dashboard | ✅ | ✅ | ✅ | | Custom injection patterns | ❌ | ✅ | ✅ | | Custom Iron Dome policies | ❌ | ✅ | ✅ | | Custom firewall rules | ❌ | ✅ | ✅ | | Audit export | ❌ | ✅ | ✅ | | Deep skill scanning | ❌ | ✅ | ✅ | | Cortex (mistake learning) | ❌ | ✅ | ✅ | | Cloud sync | ❌ | ❌ | ✅ | | Team management | ❌ | ❌ | ✅ | | Shared patterns | ❌ | ❌ | ✅ | ## Links - **Docs:** https://shieldcortex.ai/docs - **Source:** https://github.com/Drakon-Systems-Ltd/ShieldCortex - **npm:** https://www.npmjs.com/package/shieldcortex - **Issues:** https://github.com/Drakon-Systems-Ltd/ShieldCortex/issues - **Changelog:** https://shieldcortex.ai/changelog

path: bundled/cortex-memory-hook/HOOK.md
content: ---
name: cortex-memory
description: "Persistent brain-like memory via ShieldCortex — recalls past knowledge, with optional auto-save"
homepage: https://github.com/Drakon-Systems-Ltd/ShieldCortex
metadata:
  {
    "openclaw":
      {
        "emoji": "🧠",
        "events": ["command:new", "command:stop", "agent:bootstrap", "command"],
        "requires": { "anyBins": ["npx"] },
        "install": [{ "id": "community", "kind": "community", "label": "ShieldCortex" }],
      },
  }
---

# Cortex Memory Hook

Integrates [ShieldCortex](https://github.com/Drakon-Systems-Ltd/ShieldCortex) persistent memory. Recalls past knowledge at session start, and can optionally auto-save important session context.

## What It Does

### On `/new` (Session End)
When `openclawAutoMemory` is enabled:
1. Reads the ending session transcript
2. Pattern-matches for decisions, bug fixes, learnings, architecture changes, and preferences
3. Saves up to 5 high-salience memories to ShieldCortex via mcporter
4. Skips exact and near-duplicate memories using novelty filtering

### On `/stop`, `/clear`, `/exit` (Session End)
When `openclawAutoMemory` is enabled:
1. Captures the current session transcript before it ends
2. Pattern-matches for important content (same patterns as `/new`)
3. Saves memories with a `session-stop` tag for tracking
4. **Ensures work is saved** even when explicitly ending a session
5. Skips exact and near-duplicate memories using novelty filtering

### On Session Start (Agent Bootstrap)
1. Calls Cortex `get_context` to retrieve relevant memories
2. Injects them into the agent's bootstrap context
3. Agent starts with knowledge of past sessions

### Keyword Triggers

Say any of these phrases to trigger an instant save to Cortex memory:

| Trigger Phrase | Category | Importance |
|---------------|----------|------------|
| **"remember this"** | note | critical |
| **"don't forget"** | note | critical |
| **"this is important"** | note | critical |
| **"make a note"** | note | critical |
| **"for the record"** | note | critical |
| **"note to self"** | note | critical |
| **"important:"** | note | critical |
| **"crucial:"** | note | critical |
| **"key point:"** | note | high |
| **"lesson learned"** | learning | high |
| **"i learned"** | learning | normal |
| **"TIL:"** | learning | normal |
| **"today i learned"** | learning | normal |
| **"never again"** | error | critical |
| **"root cause was"** | error | high |
| **"the fix was"** | error | high |
| **"always do"** | preference | high |
| **"never do"** | preference | high |
| **"i prefer"** | preference | normal |
| **"we should always"** | preference | high |
| **"we decided"** | architecture | high |
| **"decision made"** | architecture | high |
| **"going with"** | architecture | normal |

Content after the trigger phrase is extracted and saved as the memory content.

## Auto-Memory

Auto-memory extraction is enabled by default. ShieldCortex complements your existing memory system by capturing decisions, fixes, and learnings with built-in deduplication to avoid noise.

Disable auto-save with CLI:

```bash
npx shieldcortex config --openclaw-auto-memory false
```

Re-enable it:

```bash
npx shieldcortex config --openclaw-auto-memory true
```

Or set directly in config:

```json
{
  "openclawAutoMemory": true
}
```

in `~/.shieldcortex/config.json`.

## Requirements

- **npx** must be available (Node.js installed)
- ShieldCortex installs automatically on first use via `npx -y shieldcortex`
- mcporter must be available for MCP tool calls

## Database

Memories stored in `~/.shieldcortex/memories.db` (SQLite). Shared with Claude Code sessions — memories created here are available everywhere.

## Install

```bash
openclaw skills install shieldcortex
```

Optional companion real-time plugin:

```bash
openclaw plugins install @drakon-systems/shieldcortex-realtime
```

## Uninstall

```bash
shieldcortex openclaw uninstall
```

Or disable without removing:

```json
{
  "hooks": {
    "internal": {
      "entries": {
        "cortex-memory": { "enabled": false }
      }
    }
  }
}
```


path: bundled/cortex-memory-hook/runtime.mjs
content: import { execFile } from "node:child_process";
import fs from "node:fs/promises";
import { homedir } from "node:os";
import path from "node:path";

export function createOpenClawRuntime({ logPrefix = "[shieldcortex]" } = {}) {
  let shieldConfig = null;
  let resolvedServerCmd = null;
  let lastCallErrorType = null;

  async function loadShieldConfig() {
    if (shieldConfig) return shieldConfig;

    try {
      const configPath = path.join(homedir(), ".shieldcortex", "config.json");
      shieldConfig = JSON.parse(await fs.readFile(configPath, "utf-8"));
    } catch {
      shieldConfig = {};
    }

    return shieldConfig;
  }

  function isOpenClawAutoMemoryEnabled(config) {
    return config?.openclawAutoMemory === true;
  }

  function classifyCallError(err) {
    if (err.killed || err.code === "ETIMEDOUT" || err.signal === "SIGTERM") return "timeout";
    if (/ENOENT|not found|command not found/i.test(err.message || "")) return "not-found";
    if (/mcporter/i.test(err.message || "")) return "mcporter";
    return "unknown";
  }

  async function resolveServerCmd() {
    if (resolvedServerCmd) return resolvedServerCmd;

    try {
      const config = await loadShieldConfig();
      if (config?.binaryPath) {
        await fs.access(config.binaryPath);
        resolvedServerCmd = config.binaryPath;
        console.log(`${logPrefix} Using configured binary: ${config.binaryPath}`);
        return resolvedServerCmd;
      }
    } catch {}

    try {
      const { execFileSync } = await import("node:child_process");
      const bin = execFileSync("which", ["shieldcortex"], {
        encoding: "utf-8",
        timeout: 3000,
      }).trim();
      if (bin) {
        resolvedServerCmd = bin;
        console.log(`${logPrefix} Using global install: ${bin}`);
        return resolvedServerCmd;
      }
    } catch {}

    resolvedServerCmd = "npx -y shieldcortex";
    console.log(`${logPrefix} Falling back to npx -y shieldcortex (slow path)`);
    return resolvedServerCmd;
  }

  async function callCortex(tool, args = {}, options = { retries: 0, timeout: 15000 }) {
    const serverCmd = await resolveServerCmd();

    return new Promise((resolve) => {
      const cmdArgs = ["mcporter", "call", "--stdio", serverCmd, tool];
      for (const [key, value] of Object.entries(args)) {
        cmdArgs.push(`${key}:${String(value).replace(/'/g, "''")}`);
      }

      let attempts = 0;
      const maxAttempts = (options.retries || 0) + 1;

      function attempt() {
        attempts++;
        execFile("npx", cmdArgs, {
          timeout: options.timeout || 15000,
          maxBuffer: 256 * 1024,
        }, (err, stdout) => {
          if (err) {
            const isTimeout = err.killed || err.code === "ETIMEDOUT" || err.signal === "SIGTERM";
            const errorType = isTimeout ? "TIMEOUT" : "ERROR";

            if (attempts < maxAttempts) {
              console.warn(`${logPrefix} ${errorType} on ${tool} (attempt ${attempts}/${maxAttempts}), retrying...`);
              setTimeout(attempt, 1000);
              return;
            }

            const category = classifyCallError(err);
            if (category !== lastCallErrorType) {
              lastCallErrorType = category;
              switch (category) {
                case "timeout":
                  console.warn(`${logPrefix} ShieldCortex call timed out (15s). Memory may be under heavy load.`);
                  break;
                case "not-found":
                  console.warn(`${logPrefix} ShieldCortex binary not found. Run: npm install -g shieldcortex`);
                  break;
                case "mcporter":
                  console.warn(`${logPrefix} mcporter failed to reach ShieldCortex MCP server. Is it configured?`);
                  break;
                default:
                  console.warn(`${logPrefix} ShieldCortex call failed: ${err.message}`);
              }
            }
            resolve(null);
            return;
          }

          resolve(stdout?.trim() || null);
        });
      }

      attempt();
    });
  }

  return {
    callCortex,
    isOpenClawAutoMemoryEnabled,
    loadShieldConfig,
    resolveServerCmd,
  };
}


path: bundled/openclaw-plugin/index.js
content: /**
 * ShieldCortex Real-time Scanning Plugin for OpenClaw v2026.3.22+
 *
 * Uses explicit capability registration (registerHook + registerCommand)
 * for llm_input/llm_output scanning and optional memory extraction.
 * All scanning operations are fire-and-forget.
 */
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import fs from "node:fs/promises";
import { existsSync, readFileSync, realpathSync } from "node:fs";
import path from "node:path";
import { homedir } from "node:os";
import { fileURLToPath, pathToFileURL } from "node:url";
let runtimePromise = null;
function addRuntimeCandidate(candidates, packageRoot) {
    const runtimePath = path.join(packageRoot, "hooks", "openclaw", "cortex-memory", "runtime.mjs");
    if (existsSync(runtimePath)) {
        candidates.add(pathToFileURL(runtimePath).href);
    }
}
function addAncestorCandidates(candidates, startPath) {
    let current = path.resolve(startPath);
    let previous = "";
    for (let i = 0; i < 6 && current !== previous; i++) {
        addRuntimeCandidate(candidates, current);
        previous = current;
        current = path.dirname(current);
    }
}
function collectRuntimeCandidates() {
    const candidates = new Set();
    // 1. Relative path (works when running from within npm package tree)
    candidates.add(new URL("../../hooks/openclaw/cortex-memory/runtime.mjs", import.meta.url).href);
    // 2. Environment variable override
    if (process.env.SHIELDCORTEX_ROOT) {
        addRuntimeCandidate(candidates, process.env.SHIELDCORTEX_ROOT);
    }
    // 3. Walk up from current file location
    addAncestorCandidates(candidates, path.dirname(fileURLToPath(import.meta.url)));
    // 4. Find via shieldcortex binary
    try {
        const bin = execFileSync("which", ["shieldcortex"], {
            encoding: "utf-8",
            timeout: 3000,
        }).trim();
        if (bin)
            addAncestorCandidates(candidates, realpathSync(bin));
    }
    catch { }
    // 5. npm global root
    try {
        const npmRoot = execFileSync("npm", ["root", "-g"], {
            encoding: "utf-8",
            timeout: 3000,
        }).trim();
        if (npmRoot)
            addRuntimeCandidate(candidates, path.join(npmRoot, "shieldcortex"));
    }
    catch { }
    // 6. npm prefix
    try {
        const prefix = execFileSync("npm", ["config", "get", "prefix"], {
            encoding: "utf-8",
            timeout: 3000,
        }).trim();
        if (prefix)
            addRuntimeCandidate(candidates, path.join(prefix, "lib", "node_modules", "shieldcortex"));
    }
    catch { }
    // 7. Common global install paths
    for (const root of [
        "/usr/lib/node_modules/shieldcortex",
        "/usr/local/lib/node_modules/shieldcortex",
        "/opt/homebrew/lib/node_modules/shieldcortex",
        path.join(homedir(), ".npm-global", "lib", "node_modules", "shieldcortex"),
    ]) {
        addRuntimeCandidate(candidates, root);
    }
    return [...candidates];
}
async function getRuntime() {
    if (!runtimePromise) {
        runtimePromise = (async () => {
            const tried = [];
            let lastError = null;
            for (const candidate of collectRuntimeCandidates()) {
                tried.push(candidate);
                try {
                    const mod = await import(candidate);
                    if (typeof mod.createOpenClawRuntime === "function") {
                        return mod.createOpenClawRuntime({ logPrefix: "[shieldcortex]" });
                    }
                }
                catch (error) {
                    lastError = error;
                }
            }
            const detail = lastError instanceof Error ? lastError.message : String(lastError ?? "unknown error");
            throw new Error(`Could not load OpenClaw runtime. Tried: ${tried.join(", ")}. Last error: ${detail}`);
        })();
    }
    return runtimePromise;
}
const PLUGIN_ID = "shieldcortex-realtime";
const PLUGIN_PACKAGE_NAME = "@drakon-systems/shieldcortex-realtime";
const PLUGIN_CONFIG_UI_HINTS = {
    binaryPath: {
        label: "ShieldCortex Binary Path",
        help: "Optional absolute path to the shieldcortex CLI when it is not on PATH.",
        placeholder: "/usr/local/bin/shieldcortex",
        advanced: true,
    },
    cloudApiKey: {
        label: "Cloud API Key",
        help: "Optional ShieldCortex Cloud API key used for realtime threat forwarding.",
        sensitive: true,
        placeholder: "sc_...",
    },
    cloudBaseUrl: {
        label: "Cloud Base URL",
        help: "Override the ShieldCortex Cloud API base URL if you use a self-hosted or staging endpoint.",
        placeholder: "https://api.shieldcortex.ai",
        advanced: true,
    },
    openclawAutoMemory: {
        label: "Auto Memory Extraction",
        help: "Extract high-signal decisions and learnings from LLM output into ShieldCortex memory.",
    },
    openclawAutoMemoryDedupe: {
        label: "Dedupe Auto Memory",
        help: "Skip near-duplicate memories before they are written to ShieldCortex.",
        advanced: true,
    },
    openclawAutoMemoryNoveltyThreshold: {
        label: "Novelty Threshold",
        help: "Similarity threshold for duplicate suppression. Higher values keep more memories.",
        advanced: true,
    },
    openclawAutoMemoryMaxRecent: {
        label: "Recent Memory Cache Size",
        help: "How many recent extracted memories to keep in the dedupe cache.",
        advanced: true,
    },
};
const PLUGIN_CONFIG_JSON_SCHEMA = {
    type: "object",
    additionalProperties: false,
    properties: {
        enabled: { type: "boolean" },
        binaryPath: { type: "string" },
        cloudApiKey: { type: "string" },
        cloudBaseUrl: { type: "string" },
        openclawAutoMemory: { type: "boolean" },
        openclawAutoMemoryDedupe: { type: "boolean" },
        openclawAutoMemoryNoveltyThreshold: { type: "number", minimum: 0.6, maximum: 0.99 },
        openclawAutoMemoryMaxRecent: { type: "integer", minimum: 50, maximum: 1000 },
    },
};
let _config = null;
let _configOverride = null;
let _version = "0.0.0";
try {
    for (const packageUrl of [
        new URL("./package.json", import.meta.url),
        new URL("../../package.json", import.meta.url),
    ]) {
        try {
            const pkg = JSON.parse(readFileSync(packageUrl, "utf-8"));
            if (typeof pkg.version === "string" && pkg.version.trim()) {
                _version = pkg.version;
                break;
            }
        }
        catch {
            // try the next candidate
        }
    }
}
catch { /* fallback */ }
function normaliseConfig(raw) {
    if (!raw || typeof raw !== "object" || Array.isArray(raw))
        return {};
    const value = raw;
    const config = {};
    if (typeof value.cloudApiKey === "string" && value.cloudApiKey.trim()) {
        config.cloudApiKey = value.cloudApiKey.trim();
    }
    if (typeof value.cloudBaseUrl === "string" && value.cloudBaseUrl.trim()) {
        config.cloudBaseUrl = value.cloudBaseUrl.trim();
    }
    if (typeof value.binaryPath === "string" && value.binaryPath.trim()) {
        config.binaryPath = value.binaryPath.trim();
    }
    if (typeof value.openclawAutoMemory === "boolean") {
        config.openclawAutoMemory = value.openclawAutoMemory;
    }
    if (typeof value.openclawAutoMemoryDedupe === "boolean") {
        config.openclawAutoMemoryDedupe = value.openclawAutoMemoryDedupe;
    }
    if (typeof value.openclawAutoMemoryNoveltyThreshold === "number" && !Number.isNaN(value.openclawAutoMemoryNoveltyThreshold)) {
        config.openclawAutoMemoryNoveltyThreshold = clamp(value.openclawAutoMemoryNoveltyThreshold, 0.6, 0.99);
    }
    if (typeof value.openclawAutoMemoryMaxRecent === "number" && !Number.isNaN(value.openclawAutoMemoryMaxRecent)) {
        config.openclawAutoMemoryMaxRecent = Math.floor(clamp(value.openclawAutoMemoryMaxRecent, 50, 1000));
    }
    return config;
}
function extractPluginConfig(rootConfig) {
    if (!rootConfig || typeof rootConfig !== "object" || Array.isArray(rootConfig))
        return {};
    const entries = rootConfig.plugins?.entries;
    const pluginConfig = entries?.[PLUGIN_ID]?.config ??
        entries?.[PLUGIN_PACKAGE_NAME]?.config;
    return normaliseConfig(pluginConfig);
}
function applyPluginConfigOverride(api) {
    const runtimeConfig = typeof api.runtime?.config?.loadConfig === "function"
        ? api.runtime.config.loadConfig()
        : api.config;
    const pluginConfig = extractPluginConfig(runtimeConfig);
    if (Object.keys(pluginConfig).length === 0)
        return;
    _configOverride = {
        ...(_configOverride ?? {}),
        ...pluginConfig,
    };
    if (_config) {
        _config = { ..._config, ...pluginConfig };
    }
}
async function loadConfig() {
    if (_config)
        return _config;
    const shieldConfig = normaliseConfig(await (await getRuntime()).loadShieldConfig());
    _config = {
        ...shieldConfig,
        ...(_configOverride ?? {}),
    };
    return _config;
}
function isAutoMemoryEnabled(config) {
    return config.openclawAutoMemory === true;
}
function isAutoMemoryDedupeEnabled(config) {
    return config.openclawAutoMemoryDedupe !== false;
}
async function callCortex(tool, args = {}) {
    return (await getRuntime()).callCortex(tool, args);
}
// ==================== REMOTE SCANNING ====================
async function scanRealtimeContent(text) {
    const response = await callCortex("scan_tool_response", {
        toolName: "openclaw-realtime",
        content: text,
        mode: "advisory",
    });
    if (!response) {
        return { clean: true, summary: "scan unavailable" };
    }
    const cleanMatch = response.match(/\*\*Clean:\*\*\s*(Yes|No)/i);
    const riskMatch = response.match(/\*\*Risk Level:\*\*\s*([A-Za-z]+)/i);
    const detectionsMatch = response.match(/\*\*Detections:\*\*\s*(\d+)/i);
    const clean = cleanMatch ? /yes/i.test(cleanMatch[1]) : true;
    const risk = riskMatch?.[1] ?? "unknown";
    const detections = detectionsMatch?.[1];
    const summary = detections ? `${risk} (${detections} detections)` : risk;
    return { clean, summary };
}
// ==================== CONTENT PATTERNS ====================
const PATTERNS = {
    architecture: [/\b(?:architecture|designed|structured)\b.*?(?:uses?|is|with)\b/i, /\b(?:decided?\s+to|going\s+with|chose)\b/i],
    error: [/\b(?:fixed|resolved|solved)\s+(?:by|with|using)\b/i, /\b(?:solution|fix|root\s*cause)\s+(?:was|is)\b/i],
    learning: [/\b(?:learned|discovered|turns?\s+out|figured\s+out|realized)\b/i],
    preference: [
        /\b(?:I|we|you\s+should)\s+(?:always|never)\b/i,
        /\b(?:always\s+use|never\s+use|never\s+commit)\b/i,
        /\bprefer(?:\s+to)?\s+\w+/i,
        /\bshould\s+always\b/i,
    ],
    note: [/\b(?:important|remember|key\s+point)\s*:/i],
};
function extractMemories(texts) {
    const out = [];
    const seen = new Set();
    for (const text of texts) {
        if (text.length < 30)
            continue;
        for (const [cat, pats] of Object.entries(PATTERNS)) {
            if (pats.some(p => p.test(text))) {
                const title = text.slice(0, 80).replace(/["\n]/g, " ").trim();
                if (!seen.has(title)) {
                    seen.add(title);
                    out.push({ title, content: text.slice(0, 500), category: cat });
                }
                break;
            }
            if (out.length >= 3)
                break;
        }
        if (out.length >= 3)
            break;
    }
    return out;
}
// ==================== HELPERS ====================
function extractUserContent(msgs) {
    const out = [];
    for (const msg of msgs) {
        if (!msg || typeof msg !== "object")
            continue;
        const m = msg;
        if (m.role !== "user")
            continue;
        if (typeof m.content === "string")
            out.push(m.content);
        else if (Array.isArray(m.content))
            for (const b of m.content)
                if (b?.type === "text")
                    out.push(b.text);
    }
    return out;
}
const AUDIT_DIR = path.join(homedir(), ".shieldcortex", "audit");
const NOVELTY_CACHE_FILE = path.join(homedir(), ".shieldcortex", "openclaw-memory-cache.json");
const DEFAULT_NOVELTY_THRESHOLD = 0.88;
const DEFAULT_MAX_RECENT = 300;
const MIN_NOVELTY_CHARS = 40;
async function auditLog(entry) {
    try {
        await fs.mkdir(AUDIT_DIR, { recursive: true });
        await fs.appendFile(path.join(AUDIT_DIR, `realtime-${new Date().toISOString().slice(0, 10)}.jsonl`), JSON.stringify(entry) + "\n");
    }
    catch { }
}
async function cloudSync(threat) {
    const cfg = await loadConfig();
    if (!cfg.cloudApiKey)
        return;
    try {
        await fetch(`${cfg.cloudBaseUrl || "https://api.shieldcortex.ai"}/v1/threats`, {
            method: "POST",
            headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.cloudApiKey}` },
            body: JSON.stringify(threat),
            signal: AbortSignal.timeout(5000),
        });
    }
    catch { }
}
function normalizeMemoryText(text) {
    return String(text || "")
        .toLowerCase()
        .replace(/[`"'\\]/g, " ")
        .replace(/https?:\/\/\S+/g, " ")
        .replace(/[^a-z0-9\s]/g, " ")
        .replace(/\s+/g, " ")
        .trim();
}
function hashToken(token) {
    return createHash("sha1").update(token).digest("hex").slice(0, 12);
}
function buildTokenHashes(normalized) {
    const words = normalized.split(" ").filter((w) => w.length >= 3);
    const set = new Set();
    for (let i = 0; i < words.length; i++) {
        set.add(hashToken(words[i]));
        if (i < words.length - 1)
            set.add(hashToken(`${words[i]}_${words[i + 1]}`));
    }
    return Array.from(set).slice(0, 200);
}
function jaccardSimilarity(a, b) {
    if (a.size === 0 || b.size === 0)
        return 0;
    let intersection = 0;
    for (const item of a) {
        if (b.has(item))
            intersection++;
    }
    const union = a.size + b.size - intersection;
    return union === 0 ? 0 : intersection / union;
}
function clamp(value, min, max) {
    return Math.max(min, Math.min(max, value));
}
async function loadNoveltyCache(maxRecent) {
    try {
        const raw = JSON.parse(await fs.readFile(NOVELTY_CACHE_FILE, "utf-8"));
        if (!Array.isArray(raw))
            return [];
        return raw
            .filter((entry) => entry && typeof entry.hash === "string" && Array.isArray(entry.tokenHashes))
            .slice(0, maxRecent);
    }
    catch {
        return [];
    }
}
async function saveNoveltyCache(entries) {
    await fs.mkdir(path.dirname(NOVELTY_CACHE_FILE), { recursive: true });
    await fs.writeFile(NOVELTY_CACHE_FILE, JSON.stringify(entries, null, 2) + "\n", "utf-8");
}
function inspectNovelty(content, entries, threshold) {
    const normalized = normalizeMemoryText(content);
    if (normalized.length < MIN_NOVELTY_CHARS) {
        return { allow: true, contentHash: null, tokenHashes: [] };
    }
    const contentHash = createHash("sha256").update(normalized).digest("hex").slice(0, 24);
    if (entries.some((entry) => entry.hash === contentHash)) {
        return { allow: false, contentHash, tokenHashes: [], reason: "exact duplicate" };
    }
    const tokenHashes = buildTokenHashes(normalized);
    const currentSet = new Set(tokenHashes);
    for (const entry of entries) {
        const score = jaccardSimilarity(currentSet, new Set(entry.tokenHashes || []));
        if (score >= threshold) {
            return {
                allow: false,
                contentHash,
                tokenHashes,
                reason: `near duplicate (similarity ${score.toFixed(2)})`,
            };
        }
    }
    return { allow: true, contentHash, tokenHashes };
}
async function createNoveltyGate(config) {
    const thresholdRaw = Number(config.openclawAutoMemoryNoveltyThreshold);
    const maxRecentRaw = Number(config.openclawAutoMemoryMaxRecent);
    const threshold = Number.isFinite(thresholdRaw)
        ? clamp(thresholdRaw, 0.6, 0.99)
        : DEFAULT_NOVELTY_THRESHOLD;
    const maxRecent = Number.isFinite(maxRecentRaw)
        ? Math.floor(clamp(maxRecentRaw, 50, 1000))
        : DEFAULT_MAX_RECENT;
    const enabled = isAutoMemoryDedupeEnabled(config);
    const entries = enabled ? await loadNoveltyCache(maxRecent) : [];
    let dirty = false;
    return {
        inspect(content) {
            if (!enabled)
                return { allow: true, contentHash: null, tokenHashes: [] };
            return inspectNovelty(content, entries, threshold);
        },
        remember(memory, novelty) {
            if (!enabled || !novelty.contentHash || novelty.tokenHashes.length === 0)
                return;
            entries.unshift({
                hash: novelty.contentHash,
                tokenHashes: novelty.tokenHashes,
                title: String(memory.title || "").slice(0, 120),
                category: String(memory.category || "note"),
                createdAt: new Date().toISOString(),
            });
            if (entries.length > maxRecent)
                entries.length = maxRecent;
            dirty = true;
        },
        async flush() {
            if (!enabled || !dirty)
                return;
            await saveNoveltyCache(entries);
        },
    };
}
// ==================== HOOK HANDLERS ====================
// Skip scanning internal OpenClaw content (boot checks, system prompts, heartbeats)
const SKIP_PATTERNS = [
    /^You are running a boot check/i,
    /^Read HEARTBEAT\.md/i,
    /^System:/,
    /^\[System Message\]/,
    /^HEARTBEAT_OK$/,
    /^\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s/, // Timestamped system events
    /^A subagent task/i,
    /subagent.*completed/i,
];
function isInternalContent(text) {
    return SKIP_PATTERNS.some(p => p.test(text.trim()));
}
function handleLlmInput(event, ctx) {
    // Fire and forget
    (async () => {
        try {
            // Only scan user content, skip system/boot/heartbeat prompts
            const userTexts = extractUserContent(event.historyMessages).slice(-5);
            const texts = [event.prompt, ...userTexts].filter(t => t && !isInternalContent(t));
            for (const text of texts) {
                if (!text || text.length < 10)
                    continue;
                const result = await scanRealtimeContent(text);
                if (!result.clean) {
                    console.warn(`[shieldcortex] ⚠️ Threat in LLM input: ${result.summary}`);
                    const entry = {
                        type: "threat", hook: "llm_input", sessionId: event.sessionId,
                        model: event.model, reason: result.summary,
                        preview: text.slice(0, 100), ts: new Date().toISOString(),
                    };
                    auditLog(entry);
                    cloudSync({ ...entry, content: text.slice(0, 200) });
                }
            }
        }
        catch (e) {
            console.error("[shieldcortex] llm_input error:", e instanceof Error ? e.message : String(e));
        }
    })();
}
// Skip text blocks that are ShieldCortex/OpenClaw tool-result pass-throughs
function isToolResultContent(text) {
    // ShieldCortex recall returns "Found N memories:" header
    if (/^Found \d+ memor(?:y|ies):/m.test(text))
        return true;
    // ShieldCortex get_context returns structured context blocks
    if (/^## (?:Architecture|Patterns|Preferences|Errors|Context)/m.test(text))
        return true;
    // OpenClaw tool-result wrapper markers
    if (/^\[tool_result\b/i.test(text.trim()))
        return true;
    if (/^<tool_result\b/i.test(text.trim()))
        return true;
    return false;
}
function handleLlmOutput(event, ctx) {
    // Fire and forget
    (async () => {
        try {
            const config = await loadConfig();
            if (!isAutoMemoryEnabled(config))
                return;
            const texts = event.assistantTexts
                .filter(t => t && t.length >= 30)
                .filter(t => !isToolResultContent(t));
            if (!texts.length)
                return;
            const memories = extractMemories(texts);
            if (!memories.length)
                return;
            const noveltyGate = await createNoveltyGate(config);
            let saved = 0;
            let skipped = 0;
            for (const mem of memories) {
                const novelty = noveltyGate.inspect(mem.content);
                if (!novelty.allow) {
                    skipped++;
                    continue;
                }
                const r = await callCortex("remember", {
                    title: mem.title, content: mem.content, category: mem.category,
                    project: ctx.agentId || "openclaw", scope: "global",
                    importance: "normal", tags: "auto-extracted,realtime-plugin,llm-output",
                    sourceType: "agent", sourceIdentifier: `openclaw-plugin:${event.sessionId}`,
                    sessionId: event.sessionId, agentId: ctx.agentId || "openclaw", workspaceDir: ctx.workspaceDir || "",
                });
                if (r) {
                    saved++;
                    noveltyGate.remember(mem, novelty);
                }
            }
            await noveltyGate.flush();
            if (saved) {
                console.log(`[shieldcortex] Extracted ${saved} memor${saved === 1 ? "y" : "ies"} from LLM output (${skipped} duplicates skipped)`);
                auditLog({ type: "memory", hook: "llm_output", sessionId: event.sessionId, count: saved, skipped, ts: new Date().toISOString() });
            }
        }
        catch (e) {
            console.error("[shieldcortex] llm_output error:", e instanceof Error ? e.message : String(e));
        }
    })();
}
// ==================== PLUGIN EXPORT ====================
export default {
    id: PLUGIN_ID,
    name: "ShieldCortex Real-time Scanner",
    description: "Real-time defence scanning on LLM inputs with optional memory extraction from outputs",
    version: _version,
    configSchema: {
        parse(value) {
            return normaliseConfig(value);
        },
        uiHints: PLUGIN_CONFIG_UI_HINTS,
        jsonSchema: PLUGIN_CONFIG_JSON_SCHEMA,
    },
    register(api) {
        applyPluginConfigOverride(api);
        // Explicit capability registration (replaces legacy api.on)
        api.registerHook("llm_input", handleLlmInput, {
            name: "shieldcortex-scan-input",
            description: "Real-time threat scanning on LLM input",
        });
        api.registerHook("llm_output", handleLlmOutput, {
            name: "shieldcortex-scan-output",
            description: "Memory extraction from LLM output",
        });
        // Register a lightweight status command so the plugin is not hook-only
        api.registerCommand({
            name: "shieldcortex-status",
            aliases: ["sc-status"],
            description: "Show ShieldCortex real-time scanner status",
            async execute({ reply }) {
                const cfg = await loadConfig();
                const autoMemory = isAutoMemoryEnabled(cfg) ? "on" : "off";
                const dedupe = isAutoMemoryDedupeEnabled(cfg) ? "on" : "off";
                const cloud = cfg.cloudApiKey ? "configured" : "not configured";
                reply(`ShieldCortex v${_version}\n` +
                    `  Hooks: llm_input (scan), llm_output (memory)\n` +
                    `  Auto memory: ${autoMemory} | Dedupe: ${dedupe}\n` +
                    `  Cloud sync: ${cloud}`);
            },
        });
        api.logger.info(`[shieldcortex] v${_version} registered (llm_input + llm_output + /shieldcortex-status)`);
    },
};


path: bundled/openclaw-plugin/openclaw.plugin.json
content: {
  "id": "shieldcortex-realtime",
  "version": "3.5.0",
  "name": "ShieldCortex Real-time Scanner",
  "description": "Real-time defence scanning on LLM input, memory extraction on LLM output, and active tool call interception with approval gating.",
  "uiHints": {
    "binaryPath": {
      "label": "ShieldCortex Binary Path",
      "help": "Optional absolute path to the shieldcortex CLI when it is not on PATH.",
      "placeholder": "/usr/local/bin/shieldcortex",
      "advanced": true
    },
    "cloudApiKey": {
      "label": "Cloud API Key",
      "help": "Optional ShieldCortex Cloud API key used for realtime threat forwarding.",
      "placeholder": "sc_...",
      "sensitive": true
    },
    "cloudBaseUrl": {
      "label": "Cloud Base URL",
      "help": "Override the ShieldCortex Cloud API base URL if you use a self-hosted or staging endpoint.",
      "placeholder": "https://api.shieldcortex.ai",
      "advanced": true
    },
    "openclawAutoMemory": {
      "label": "Auto Memory Extraction",
      "help": "Extract high-signal decisions and learnings from LLM output into ShieldCortex memory."
    },
    "openclawAutoMemoryDedupe": {
      "label": "Dedupe Auto Memory",
      "help": "Skip near-duplicate memories before they are written to ShieldCortex.",
      "advanced": true
    },
    "openclawAutoMemoryNoveltyThreshold": {
      "label": "Novelty Threshold",
      "help": "Similarity threshold for duplicate suppression. Higher values keep more memories.",
      "advanced": true
    },
    "openclawAutoMemoryMaxRecent": {
      "label": "Recent Memory Cache Size",
      "help": "How many recent extracted memories to keep in the dedupe cache.",
      "advanced": true
    },
    "interceptor.enabled": {
      "label": "Enable Tool Call Interceptor",
      "description": "Scan memory-write tool calls and gate suspicious content behind user approval",
      "type": "boolean"
    },
    "interceptor.severityActions.high": {
      "label": "High Severity Action",
      "description": "Action for high-severity threats (log, warn, require_approval)",
      "type": "string"
    },
    "interceptor.severityActions.critical": {
      "label": "Critical Severity Action",
      "description": "Action for critical-severity threats (log, warn, require_approval)",
      "type": "string"
    }
  },
  "configSchema": {
    "type": "object",
    "additionalProperties": true,
    "properties": {
      "enabled": {
        "type": "boolean"
      },
      "binaryPath": {
        "type": "string"
      },
      "cloudApiKey": {
        "type": "string"
      },
      "cloudBaseUrl": {
        "type": "string"
      },
      "openclawAutoMemory": {
        "type": "boolean"
      },
      "openclawAutoMemoryDedupe": {
        "type": "boolean"
      },
      "openclawAutoMemoryNoveltyThreshold": {
        "type": "number",
        "minimum": 0.6,
        "maximum": 0.99
      },
      "openclawAutoMemoryMaxRecent": {
        "type": "integer",
        "minimum": 50,
        "maximum": 1000
      },
      "interceptor": {
        "type": "object",
        "properties": {
          "enabled": { "type": "boolean", "default": true },
          "severityActions": {
            "type": "object",
            "properties": {
              "low": { "type": "string", "enum": ["log", "warn", "require_approval"], "default": "log" },
              "medium": { "type": "string", "enum": ["log", "warn", "require_approval"], "default": "warn" },
              "high": { "type": "string", "enum": ["log", "warn", "require_approval"], "default": "require_approval" },
              "critical": { "type": "string", "enum": ["log", "warn", "require_approval"], "default": "require_approval" }
            }
          },
          "failurePolicy": {
            "type": "object",
            "properties": {
              "low": { "type": "string", "enum": ["allow", "deny"], "default": "allow" },
              "medium": { "type": "string", "enum": ["allow", "deny"], "default": "allow" },
              "high": { "type": "string", "enum": ["allow", "deny"], "default": "deny" },
              "critical": { "type": "string", "enum": ["allow", "deny"], "default": "deny" }
            }
          }
        }
      }
    }
  }
}


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: suspicious | SkillSpector: suspicious (CRITICAL)