| /** 标准 MCP 接入条目:零安装,npx 拉取已发布的 shellward-mcp */ | ||
| export declare const SHELLWARD_MCP_ENTRY: { | ||
| command: string; | ||
| args: string[]; | ||
| }; | ||
| export interface InitTarget { | ||
| name: string; | ||
| path: string; | ||
| /** 配置里放 MCP 服务器的字段名(绝大多数是 mcpServers) */ | ||
| key: string; | ||
| /** 工具未安装时是否允许新建配置文件 */ | ||
| createIfMissing?: boolean; | ||
| } | ||
| /** 已知 AI 工具的 MCP 配置位置(跨平台) */ | ||
| export declare function knownTargets(home?: string): InitTarget[]; | ||
| export type MergeResult = { | ||
| status: 'added' | 'updated'; | ||
| config: any; | ||
| } | { | ||
| status: 'unchanged'; | ||
| config: any; | ||
| }; | ||
| /** | ||
| * 纯合并:把 shellward 条目并入配置对象。已存在且相同→unchanged;不同→updated;没有→added。 | ||
| * 不破坏其它 MCP 服务器条目。 | ||
| */ | ||
| export declare function mergeShellward(config: any, key: string): MergeResult; | ||
| export interface InitOutcome { | ||
| name: string; | ||
| path: string; | ||
| result: 'added' | 'updated' | 'unchanged' | 'skipped' | 'error'; | ||
| detail?: string; | ||
| } | ||
| /** 执行接入:探测→读取→合并→备份→写回。dryRun 仅预览不写。 */ | ||
| export declare function runInit(opts?: { | ||
| dryRun?: boolean; | ||
| home?: string; | ||
| }): InitOutcome[]; |
+77
| // src/init.ts — `shellward init`:一条命令把 ShellWard 接入已安装的 AI 工具(MCP 运行时防护) | ||
| // | ||
| // 把"扫描 → 运行时防护"的部署摩擦降到一条命令:自动探测 Claude Desktop / Cursor / | ||
| // Claude Code / Windsurf 的 MCP 配置,安全地加入 shellward 条目(备份、合并、不覆盖)。 | ||
| // 这是「安装按钮」的正确形态——只对已知配置文件操作、改前备份、可 --dry-run 预览。 | ||
| import { readFileSync, writeFileSync, existsSync, copyFileSync, mkdirSync } from 'fs'; | ||
| import { join, dirname } from 'path'; | ||
| import { homedir } from 'os'; | ||
| /** 标准 MCP 接入条目:零安装,npx 拉取已发布的 shellward-mcp */ | ||
| export const SHELLWARD_MCP_ENTRY = { | ||
| command: 'npx', | ||
| args: ['-y', '-p', 'shellward', 'shellward-mcp'], | ||
| }; | ||
| /** 已知 AI 工具的 MCP 配置位置(跨平台) */ | ||
| export function knownTargets(home = homedir()) { | ||
| const appData = process.env.APPDATA || join(home, 'AppData', 'Roaming'); | ||
| const claudeDesktop = process.platform === 'darwin' ? join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json') | ||
| : process.platform === 'win32' ? join(appData, 'Claude', 'claude_desktop_config.json') | ||
| : join(home, '.config', 'Claude', 'claude_desktop_config.json'); | ||
| return [ | ||
| { name: 'Claude Desktop', path: claudeDesktop, key: 'mcpServers', createIfMissing: true }, | ||
| { name: 'Cursor', path: join(home, '.cursor', 'mcp.json'), key: 'mcpServers', createIfMissing: true }, | ||
| { name: 'Claude Code', path: join(home, '.claude.json'), key: 'mcpServers' }, | ||
| { name: 'Windsurf', path: join(home, '.codeium', 'windsurf', 'mcp_config.json'), key: 'mcpServers' }, | ||
| ]; | ||
| } | ||
| /** | ||
| * 纯合并:把 shellward 条目并入配置对象。已存在且相同→unchanged;不同→updated;没有→added。 | ||
| * 不破坏其它 MCP 服务器条目。 | ||
| */ | ||
| export function mergeShellward(config, key) { | ||
| const cfg = config && typeof config === 'object' ? config : {}; | ||
| const servers = cfg[key] && typeof cfg[key] === 'object' ? cfg[key] : {}; | ||
| const existing = servers.shellward; | ||
| const same = existing && JSON.stringify(existing) === JSON.stringify(SHELLWARD_MCP_ENTRY); | ||
| if (same) | ||
| return { status: 'unchanged', config: cfg }; | ||
| const status = existing ? 'updated' : 'added'; | ||
| cfg[key] = { ...servers, shellward: { ...SHELLWARD_MCP_ENTRY } }; | ||
| return { status, config: cfg }; | ||
| } | ||
| /** 执行接入:探测→读取→合并→备份→写回。dryRun 仅预览不写。 */ | ||
| export function runInit(opts = {}) { | ||
| const targets = knownTargets(opts.home); | ||
| const out = []; | ||
| for (const t of targets) { | ||
| const exists = existsSync(t.path); | ||
| if (!exists && !t.createIfMissing) { | ||
| out.push({ name: t.name, path: t.path, result: 'skipped', detail: '未安装/无配置' }); | ||
| continue; | ||
| } | ||
| try { | ||
| let config = {}; | ||
| if (exists) { | ||
| const raw = readFileSync(t.path, 'utf-8').trim(); | ||
| config = raw ? JSON.parse(raw) : {}; | ||
| } | ||
| const merged = mergeShellward(config, t.key); | ||
| if (merged.status === 'unchanged') { | ||
| out.push({ name: t.name, path: t.path, result: 'unchanged', detail: '已接入' }); | ||
| continue; | ||
| } | ||
| if (!opts.dryRun) { | ||
| if (exists) | ||
| copyFileSync(t.path, t.path + '.shellward.bak'); // 改前备份 | ||
| else | ||
| mkdirSync(dirname(t.path), { recursive: true }); | ||
| writeFileSync(t.path, JSON.stringify(merged.config, null, 2) + '\n'); | ||
| } | ||
| out.push({ name: t.name, path: t.path, result: merged.status, detail: opts.dryRun ? '预览(未写入)' : (exists ? '已加入(原文件已备份 .bak)' : '已新建配置') }); | ||
| } | ||
| catch (e) { | ||
| out.push({ name: t.name, path: t.path, result: 'error', detail: e?.message || String(e) }); | ||
| } | ||
| } | ||
| return out; | ||
| } |
+99
| // src/init.ts — `shellward init`:一条命令把 ShellWard 接入已安装的 AI 工具(MCP 运行时防护) | ||
| // | ||
| // 把"扫描 → 运行时防护"的部署摩擦降到一条命令:自动探测 Claude Desktop / Cursor / | ||
| // Claude Code / Windsurf 的 MCP 配置,安全地加入 shellward 条目(备份、合并、不覆盖)。 | ||
| // 这是「安装按钮」的正确形态——只对已知配置文件操作、改前备份、可 --dry-run 预览。 | ||
| import { readFileSync, writeFileSync, existsSync, copyFileSync, mkdirSync } from 'fs' | ||
| import { join, dirname } from 'path' | ||
| import { homedir } from 'os' | ||
| /** 标准 MCP 接入条目:零安装,npx 拉取已发布的 shellward-mcp */ | ||
| export const SHELLWARD_MCP_ENTRY = { | ||
| command: 'npx', | ||
| args: ['-y', '-p', 'shellward', 'shellward-mcp'], | ||
| } | ||
| export interface InitTarget { | ||
| name: string | ||
| path: string | ||
| /** 配置里放 MCP 服务器的字段名(绝大多数是 mcpServers) */ | ||
| key: string | ||
| /** 工具未安装时是否允许新建配置文件 */ | ||
| createIfMissing?: boolean | ||
| } | ||
| /** 已知 AI 工具的 MCP 配置位置(跨平台) */ | ||
| export function knownTargets(home = homedir()): InitTarget[] { | ||
| const appData = process.env.APPDATA || join(home, 'AppData', 'Roaming') | ||
| const claudeDesktop = | ||
| process.platform === 'darwin' ? join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json') | ||
| : process.platform === 'win32' ? join(appData, 'Claude', 'claude_desktop_config.json') | ||
| : join(home, '.config', 'Claude', 'claude_desktop_config.json') | ||
| return [ | ||
| { name: 'Claude Desktop', path: claudeDesktop, key: 'mcpServers', createIfMissing: true }, | ||
| { name: 'Cursor', path: join(home, '.cursor', 'mcp.json'), key: 'mcpServers', createIfMissing: true }, | ||
| { name: 'Claude Code', path: join(home, '.claude.json'), key: 'mcpServers' }, | ||
| { name: 'Windsurf', path: join(home, '.codeium', 'windsurf', 'mcp_config.json'), key: 'mcpServers' }, | ||
| ] | ||
| } | ||
| export type MergeResult = | ||
| | { status: 'added' | 'updated'; config: any } | ||
| | { status: 'unchanged'; config: any } | ||
| /** | ||
| * 纯合并:把 shellward 条目并入配置对象。已存在且相同→unchanged;不同→updated;没有→added。 | ||
| * 不破坏其它 MCP 服务器条目。 | ||
| */ | ||
| export function mergeShellward(config: any, key: string): MergeResult { | ||
| const cfg = config && typeof config === 'object' ? config : {} | ||
| const servers = cfg[key] && typeof cfg[key] === 'object' ? cfg[key] : {} | ||
| const existing = servers.shellward | ||
| const same = existing && JSON.stringify(existing) === JSON.stringify(SHELLWARD_MCP_ENTRY) | ||
| if (same) return { status: 'unchanged', config: cfg } | ||
| const status = existing ? 'updated' : 'added' | ||
| cfg[key] = { ...servers, shellward: { ...SHELLWARD_MCP_ENTRY } } | ||
| return { status, config: cfg } | ||
| } | ||
| export interface InitOutcome { | ||
| name: string | ||
| path: string | ||
| result: 'added' | 'updated' | 'unchanged' | 'skipped' | 'error' | ||
| detail?: string | ||
| } | ||
| /** 执行接入:探测→读取→合并→备份→写回。dryRun 仅预览不写。 */ | ||
| export function runInit(opts: { dryRun?: boolean; home?: string } = {}): InitOutcome[] { | ||
| const targets = knownTargets(opts.home) | ||
| const out: InitOutcome[] = [] | ||
| for (const t of targets) { | ||
| const exists = existsSync(t.path) | ||
| if (!exists && !t.createIfMissing) { | ||
| out.push({ name: t.name, path: t.path, result: 'skipped', detail: '未安装/无配置' }) | ||
| continue | ||
| } | ||
| try { | ||
| let config: any = {} | ||
| if (exists) { | ||
| const raw = readFileSync(t.path, 'utf-8').trim() | ||
| config = raw ? JSON.parse(raw) : {} | ||
| } | ||
| const merged = mergeShellward(config, t.key) | ||
| if (merged.status === 'unchanged') { | ||
| out.push({ name: t.name, path: t.path, result: 'unchanged', detail: '已接入' }) | ||
| continue | ||
| } | ||
| if (!opts.dryRun) { | ||
| if (exists) copyFileSync(t.path, t.path + '.shellward.bak') // 改前备份 | ||
| else mkdirSync(dirname(t.path), { recursive: true }) | ||
| writeFileSync(t.path, JSON.stringify(merged.config, null, 2) + '\n') | ||
| } | ||
| out.push({ name: t.name, path: t.path, result: merged.status, detail: opts.dryRun ? '预览(未写入)' : (exists ? '已加入(原文件已备份 .bak)' : '已新建配置') }) | ||
| } catch (e: any) { | ||
| out.push({ name: t.name, path: t.path, result: 'error', detail: e?.message || String(e) }) | ||
| } | ||
| } | ||
| return out | ||
| } |
+34
-0
@@ -20,2 +20,3 @@ #!/usr/bin/env node | ||
| import { renderHtmlReport } from './compliance/html-report.js'; | ||
| import { runInit } from './init.js'; | ||
| import { resolveLocale } from './types.js'; | ||
@@ -35,2 +36,6 @@ const argv = process.argv.slice(2); | ||
| } | ||
| if (cmd === 'init') { | ||
| runInitCommand(argv.includes('--dry-run')); | ||
| return; | ||
| } | ||
| if (cmd === 'web') { | ||
@@ -156,2 +161,29 @@ const { startWebServer } = await import('./web/scan-server.js'); | ||
| } | ||
| /** `shellward init`:把 ShellWard 接入已安装 AI 工具的 MCP 配置(运行时防护) */ | ||
| function runInitCommand(dryRun) { | ||
| const out = runInit({ dryRun }); | ||
| console.log('\n🔌 ShellWard 接入 AI 工具运行时防护' + (dryRun ? '(预览,不写入)' : '') + '\n'); | ||
| const ICON = { added: '✅', updated: '✅', unchanged: '✔️', skipped: '·', error: '⚠️' }; | ||
| let touched = 0; | ||
| for (const o of out) { | ||
| const label = { added: '已接入', updated: '已更新', unchanged: '已接入(无变化)', skipped: '跳过', error: '失败' }[o.result]; | ||
| if (o.result === 'added' || o.result === 'updated') | ||
| touched++; | ||
| console.log(` ${ICON[o.result] || '·'} ${o.name} — ${label}${o.detail ? ':' + o.detail : ''}`); | ||
| if (o.result !== 'skipped') | ||
| console.log(` ${o.path}`); | ||
| } | ||
| console.log(''); | ||
| if (dryRun) { | ||
| console.log('这是预览。去掉 --dry-run 实际接入。'); | ||
| } | ||
| else if (touched > 0) { | ||
| console.log('✅ 已接入。请重启对应的 AI 工具,ShellWard 即作为运行时防护生效(拦注入/外泄/危险命令)。'); | ||
| console.log(' 验证:在工具里问"调用 shellward 的 security_status"。原配置已备份为 *.shellward.bak。'); | ||
| } | ||
| else { | ||
| console.log('未发现可接入的已安装 AI 工具配置。也可手动加 MCP:'); | ||
| console.log(' {"mcpServers":{"shellward":{"command":"npx","args":["-y","-p","shellward","shellward-mcp"]}}}'); | ||
| } | ||
| } | ||
| /** 跨平台在默认浏览器打开 URL 或本地文件(失败静默,不影响主流程) */ | ||
@@ -193,2 +225,3 @@ function openBrowser(target) { | ||
| shellward web --local Local web GUI: scan a local path (private, no upload) | ||
| shellward init Install ShellWard into your AI tools (MCP runtime guard) | ||
| shellward mcp Start MCP server (stdio) | ||
@@ -213,2 +246,3 @@ shellward --help | ||
| shellward web --local 本地 web GUI:填本地路径扫描(私有、不上传,客户端体验) | ||
| shellward init 一键接入你的 AI 工具(MCP 运行时防护,--dry-run 预览) | ||
| shellward mcp 启动 MCP 服务器(stdio) | ||
@@ -215,0 +249,0 @@ shellward --help |
+4
-3
| { | ||
| "name": "shellward", | ||
| "version": "0.7.13", | ||
| "version": "0.7.14", | ||
| "mcpName": "io.github.jnMetaCode/shellward", | ||
@@ -60,3 +60,3 @@ "description": "AI agent security & MCP security middleware — prompt injection detection, AI firewall, runtime guardrails & data-loss prevention for LLM tool calls. 8-layer defense against data exfiltration & dangerous commands. Zero dependencies. SDK + OpenClaw plugin. Supports LangChain, AutoGPT, Claude Code, Cursor, OpenAI Agents, Hermes Agent.", | ||
| "mcp": "npx tsx src/mcp-server.ts", | ||
| "test": "npx tsx test-sdk.ts && npx tsx test-integration.ts && npx tsx test-edge-cases.ts && npx tsx test-rugpull.ts && npx tsx test-redos.ts && npx tsx test-mcp-client.ts && npx tsx test-mcp.ts && npx tsx test-compliance.ts && npx tsx test-web.ts", | ||
| "test": "npx tsx test-sdk.ts && npx tsx test-integration.ts && npx tsx test-edge-cases.ts && npx tsx test-rugpull.ts && npx tsx test-redos.ts && npx tsx test-mcp-client.ts && npx tsx test-mcp.ts && npx tsx test-compliance.ts && npx tsx test-web.ts && npx tsx test-init.ts", | ||
| "test:redos": "npx tsx test-redos.ts", | ||
@@ -70,3 +70,4 @@ "test:compliance": "npx tsx test-compliance.ts", | ||
| "bench:scan": "npx tsx bench/scan-bench.ts", | ||
| "prepublishOnly": "npm run build" | ||
| "prepublishOnly": "npm run build", | ||
| "test:init": "npx tsx test-init.ts" | ||
| }, | ||
@@ -73,0 +74,0 @@ "openclaw": { |
+1
-1
@@ -11,3 +11,3 @@ <p align="center"> | ||
| [](./LICENSE) | ||
| [](#performance) | ||
| [](#performance) | ||
| [](#performance) | ||
@@ -14,0 +14,0 @@ |
+32
-0
@@ -21,2 +21,3 @@ #!/usr/bin/env node | ||
| import { renderHtmlReport } from './compliance/html-report.js' | ||
| import { runInit } from './init.js' | ||
| import { resolveLocale } from './types.js' | ||
@@ -40,2 +41,7 @@ | ||
| if (cmd === 'init') { | ||
| runInitCommand(argv.includes('--dry-run')) | ||
| return | ||
| } | ||
| if (cmd === 'web') { | ||
@@ -168,2 +174,26 @@ const { startWebServer } = await import('./web/scan-server.js') | ||
| /** `shellward init`:把 ShellWard 接入已安装 AI 工具的 MCP 配置(运行时防护) */ | ||
| function runInitCommand(dryRun: boolean): void { | ||
| const out = runInit({ dryRun }) | ||
| console.log('\n🔌 ShellWard 接入 AI 工具运行时防护' + (dryRun ? '(预览,不写入)' : '') + '\n') | ||
| const ICON: Record<string, string> = { added: '✅', updated: '✅', unchanged: '✔️', skipped: '·', error: '⚠️' } | ||
| let touched = 0 | ||
| for (const o of out) { | ||
| const label = { added: '已接入', updated: '已更新', unchanged: '已接入(无变化)', skipped: '跳过', error: '失败' }[o.result] | ||
| if (o.result === 'added' || o.result === 'updated') touched++ | ||
| console.log(` ${ICON[o.result] || '·'} ${o.name} — ${label}${o.detail ? ':' + o.detail : ''}`) | ||
| if (o.result !== 'skipped') console.log(` ${o.path}`) | ||
| } | ||
| console.log('') | ||
| if (dryRun) { | ||
| console.log('这是预览。去掉 --dry-run 实际接入。') | ||
| } else if (touched > 0) { | ||
| console.log('✅ 已接入。请重启对应的 AI 工具,ShellWard 即作为运行时防护生效(拦注入/外泄/危险命令)。') | ||
| console.log(' 验证:在工具里问"调用 shellward 的 security_status"。原配置已备份为 *.shellward.bak。') | ||
| } else { | ||
| console.log('未发现可接入的已安装 AI 工具配置。也可手动加 MCP:') | ||
| console.log(' {"mcpServers":{"shellward":{"command":"npx","args":["-y","-p","shellward","shellward-mcp"]}}}') | ||
| } | ||
| } | ||
| /** 跨平台在默认浏览器打开 URL 或本地文件(失败静默,不影响主流程) */ | ||
@@ -205,2 +235,3 @@ function openBrowser(target: string): void { | ||
| shellward web --local Local web GUI: scan a local path (private, no upload) | ||
| shellward init Install ShellWard into your AI tools (MCP runtime guard) | ||
| shellward mcp Start MCP server (stdio) | ||
@@ -224,2 +255,3 @@ shellward --help | ||
| shellward web --local 本地 web GUI:填本地路径扫描(私有、不上传,客户端体验) | ||
| shellward init 一键接入你的 AI 工具(MCP 运行时防护,--dry-run 预览) | ||
| shellward mcp 启动 MCP 服务器(stdio) | ||
@@ -226,0 +258,0 @@ shellward --help |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
700621
1.96%138
2.22%14226
1.93%84
5%