shellward 0.5.9 → 0.5.11

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.
Files changed (85) hide show
  1. package/README.md +133 -14
  2. package/dist/audit-log.d.ts +8 -0
  3. package/dist/audit-log.js +72 -0
  4. package/dist/auto-check.d.ts +26 -0
  5. package/dist/auto-check.js +167 -0
  6. package/dist/commands/audit.d.ts +2 -0
  7. package/dist/commands/audit.js +75 -0
  8. package/dist/commands/check-updates.d.ts +2 -0
  9. package/dist/commands/check-updates.js +166 -0
  10. package/dist/commands/harden.d.ts +2 -0
  11. package/dist/commands/harden.js +218 -0
  12. package/dist/commands/index.d.ts +2 -0
  13. package/dist/commands/index.js +56 -0
  14. package/dist/commands/scan-plugins.d.ts +2 -0
  15. package/dist/commands/scan-plugins.js +186 -0
  16. package/dist/commands/security.d.ts +2 -0
  17. package/dist/commands/security.js +109 -0
  18. package/dist/commands/upgrade-openclaw.d.ts +2 -0
  19. package/dist/commands/upgrade-openclaw.js +54 -0
  20. package/dist/core/engine.d.ts +66 -0
  21. package/dist/core/engine.js +572 -0
  22. package/dist/index.d.ts +8 -0
  23. package/dist/index.js +137 -0
  24. package/dist/layers/data-flow-guard.d.ts +2 -0
  25. package/dist/layers/data-flow-guard.js +23 -0
  26. package/dist/layers/input-auditor.d.ts +2 -0
  27. package/dist/layers/input-auditor.js +33 -0
  28. package/dist/layers/outbound-guard.d.ts +2 -0
  29. package/dist/layers/outbound-guard.js +22 -0
  30. package/dist/layers/output-scanner.d.ts +2 -0
  31. package/dist/layers/output-scanner.js +16 -0
  32. package/dist/layers/prompt-guard.d.ts +2 -0
  33. package/dist/layers/prompt-guard.js +14 -0
  34. package/dist/layers/security-gate.d.ts +2 -0
  35. package/dist/layers/security-gate.js +49 -0
  36. package/dist/layers/session-guard.d.ts +2 -0
  37. package/dist/layers/session-guard.js +34 -0
  38. package/dist/layers/tool-blocker.d.ts +2 -0
  39. package/dist/layers/tool-blocker.js +28 -0
  40. package/dist/mcp-server.d.ts +2 -0
  41. package/dist/mcp-server.js +337 -0
  42. package/dist/rules/dangerous-commands.d.ts +8 -0
  43. package/dist/rules/dangerous-commands.js +113 -0
  44. package/dist/rules/injection-en.d.ts +2 -0
  45. package/dist/rules/injection-en.js +115 -0
  46. package/dist/rules/injection-zh.d.ts +2 -0
  47. package/dist/rules/injection-zh.js +132 -0
  48. package/dist/rules/protected-paths.d.ts +2 -0
  49. package/dist/rules/protected-paths.js +75 -0
  50. package/dist/rules/sensitive-patterns.d.ts +21 -0
  51. package/dist/rules/sensitive-patterns.js +192 -0
  52. package/dist/types.d.ts +64 -0
  53. package/dist/types.js +30 -0
  54. package/dist/update-check.d.ts +40 -0
  55. package/dist/update-check.js +147 -0
  56. package/dist/utils.d.ts +4 -0
  57. package/dist/utils.js +8 -0
  58. package/openclaw.plugin.json +1 -1
  59. package/package.json +31 -8
  60. package/src/audit-log.ts +8 -4
  61. package/src/auto-check.ts +2 -2
  62. package/src/commands/audit.ts +3 -3
  63. package/src/commands/check-updates.ts +4 -4
  64. package/src/commands/harden.ts +3 -3
  65. package/src/commands/index.ts +8 -8
  66. package/src/commands/scan-plugins.ts +3 -3
  67. package/src/commands/security.ts +3 -3
  68. package/src/commands/upgrade-openclaw.ts +2 -2
  69. package/src/core/engine.ts +8 -8
  70. package/src/index.ts +17 -17
  71. package/src/layers/data-flow-guard.ts +1 -1
  72. package/src/layers/input-auditor.ts +1 -1
  73. package/src/layers/outbound-guard.ts +1 -1
  74. package/src/layers/output-scanner.ts +1 -1
  75. package/src/layers/prompt-guard.ts +1 -1
  76. package/src/layers/security-gate.ts +1 -1
  77. package/src/layers/session-guard.ts +1 -1
  78. package/src/layers/tool-blocker.ts +1 -1
  79. package/src/mcp-server.ts +386 -0
  80. package/src/rules/dangerous-commands.ts +1 -1
  81. package/src/rules/injection-en.ts +1 -1
  82. package/src/rules/injection-zh.ts +1 -1
  83. package/src/rules/protected-paths.ts +1 -1
  84. package/src/rules/sensitive-patterns.ts +1 -1
  85. package/src/update-check.ts +1 -1
@@ -0,0 +1,386 @@
1
+ #!/usr/bin/env npx tsx
2
+ // src/mcp-server.ts — ShellWard MCP Server
3
+ //
4
+ // Exposes ShellWard's 8-layer security engine as an MCP server.
5
+ // Zero dependencies — implements MCP protocol over stdio natively.
6
+ //
7
+ // Usage:
8
+ // npx tsx src/mcp-server.ts
9
+ //
10
+ // MCP config (claude_desktop_config.json / openclaw settings):
11
+ // {
12
+ // "mcpServers": {
13
+ // "shellward": {
14
+ // "command": "npx",
15
+ // "args": ["tsx", "/path/to/shellward/src/mcp-server.ts"]
16
+ // }
17
+ // }
18
+ // }
19
+
20
+ import { ShellWard } from './core/engine.js'
21
+ import { readFileSync } from 'fs'
22
+ import { fileURLToPath } from 'url'
23
+ import { dirname, join } from 'path'
24
+
25
+ const __dirname = dirname(fileURLToPath(import.meta.url))
26
+ const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'))
27
+
28
+ // ===== MCP Protocol Types =====
29
+
30
+ interface JsonRpcRequest {
31
+ jsonrpc: '2.0'
32
+ id?: number | string
33
+ method: string
34
+ params?: Record<string, unknown>
35
+ }
36
+
37
+ interface JsonRpcResponse {
38
+ jsonrpc: '2.0'
39
+ id: number | string | null
40
+ result?: unknown
41
+ error?: { code: number; message: string; data?: unknown }
42
+ }
43
+
44
+ // ===== ShellWard Instance =====
45
+
46
+ const guard = new ShellWard({
47
+ mode: (process.env.SHELLWARD_MODE as 'enforce' | 'audit') || 'enforce',
48
+ locale: (process.env.SHELLWARD_LOCALE as 'auto' | 'zh' | 'en') || 'auto',
49
+ autoCheckOnStartup: false,
50
+ layers: {
51
+ promptGuard: true,
52
+ outputScanner: true,
53
+ toolBlocker: true,
54
+ inputAuditor: true,
55
+ securityGate: true,
56
+ outboundGuard: true,
57
+ dataFlowGuard: true,
58
+ sessionGuard: true,
59
+ },
60
+ injectionThreshold: Number(process.env.SHELLWARD_THRESHOLD) || 60,
61
+ })
62
+
63
+ // ===== Tool Definitions =====
64
+
65
+ const TOOLS = [
66
+ {
67
+ name: 'check_command',
68
+ description: 'Check if a shell command is safe to execute. Detects rm -rf, reverse shells, fork bombs, curl|sh, etc.',
69
+ inputSchema: {
70
+ type: 'object' as const,
71
+ properties: {
72
+ command: { type: 'string', description: 'The shell command to check' },
73
+ },
74
+ required: ['command'],
75
+ },
76
+ },
77
+ {
78
+ name: 'check_injection',
79
+ description: 'Detect prompt injection attempts in text. Supports 32+ rules for Chinese and English, with hidden character detection.',
80
+ inputSchema: {
81
+ type: 'object' as const,
82
+ properties: {
83
+ text: { type: 'string', description: 'Text to scan for injection attempts' },
84
+ threshold: { type: 'number', description: 'Detection threshold 0-100 (default: 60, lower = stricter)' },
85
+ },
86
+ required: ['text'],
87
+ },
88
+ },
89
+ {
90
+ name: 'scan_data',
91
+ description: 'Scan text for sensitive data: PII (Chinese ID cards, phone numbers, bank cards), API keys, passwords, private keys, JWT tokens, SSN, credit cards.',
92
+ inputSchema: {
93
+ type: 'object' as const,
94
+ properties: {
95
+ text: { type: 'string', description: 'Text to scan for sensitive data' },
96
+ },
97
+ required: ['text'],
98
+ },
99
+ },
100
+ {
101
+ name: 'check_path',
102
+ description: 'Check if a file path operation is safe. Protects .env, .ssh/, .aws/credentials, private keys, /etc/passwd, etc.',
103
+ inputSchema: {
104
+ type: 'object' as const,
105
+ properties: {
106
+ path: { type: 'string', description: 'File path to check' },
107
+ operation: { type: 'string', enum: ['write', 'delete'], description: 'Operation type' },
108
+ },
109
+ required: ['path', 'operation'],
110
+ },
111
+ },
112
+ {
113
+ name: 'check_tool',
114
+ description: 'Check if a tool name is allowed. Blocks payment/transfer tools, flags exec/shell tools as sensitive.',
115
+ inputSchema: {
116
+ type: 'object' as const,
117
+ properties: {
118
+ tool_name: { type: 'string', description: 'Tool name to check (e.g. "bash", "stripe_charge", "file_read")' },
119
+ },
120
+ required: ['tool_name'],
121
+ },
122
+ },
123
+ {
124
+ name: 'check_response',
125
+ description: 'Check an AI response for security issues: canary token leaks and sensitive data exposure.',
126
+ inputSchema: {
127
+ type: 'object' as const,
128
+ properties: {
129
+ content: { type: 'string', description: 'Response content to check' },
130
+ },
131
+ required: ['content'],
132
+ },
133
+ },
134
+ {
135
+ name: 'security_status',
136
+ description: 'Get current ShellWard security status: mode, active layers, detection capabilities.',
137
+ inputSchema: {
138
+ type: 'object' as const,
139
+ properties: {},
140
+ },
141
+ },
142
+ ]
143
+
144
+ // ===== Tool Execution =====
145
+
146
+ function executeTool(name: string, args: Record<string, unknown>): unknown {
147
+ switch (name) {
148
+ case 'check_command': {
149
+ const result = guard.checkCommand(String(args.command || ''))
150
+ return {
151
+ safe: result.allowed,
152
+ level: result.level || null,
153
+ reason: result.reason || null,
154
+ rule_id: result.ruleId || null,
155
+ }
156
+ }
157
+
158
+ case 'check_injection': {
159
+ const opts = typeof args.threshold === 'number' ? { threshold: args.threshold } : undefined
160
+ const result = guard.checkInjection(String(args.text || ''), opts)
161
+ return {
162
+ safe: result.safe,
163
+ score: result.score,
164
+ threshold: result.threshold,
165
+ matched_rules: result.matched.map((m: any) => ({
166
+ id: m.id,
167
+ name: m.name,
168
+ score: m.score,
169
+ })),
170
+ hidden_chars: result.hiddenChars,
171
+ }
172
+ }
173
+
174
+ case 'scan_data': {
175
+ const result = guard.scanData(String(args.text || ''))
176
+ return {
177
+ has_sensitive_data: result.hasSensitiveData,
178
+ findings: result.findings.map((f: any) => ({
179
+ type: f.id,
180
+ name: f.name,
181
+ count: f.count,
182
+ })),
183
+ summary: result.summary,
184
+ }
185
+ }
186
+
187
+ case 'check_path': {
188
+ const op = String(args.operation || '')
189
+ if (op !== 'write' && op !== 'delete') {
190
+ throw new Error(`Invalid operation: "${op}". Must be "write" or "delete".`)
191
+ }
192
+ const result = guard.checkPath(String(args.path || ''), op)
193
+ return {
194
+ safe: result.allowed,
195
+ level: result.level || null,
196
+ reason: result.reason || null,
197
+ rule_id: result.ruleId || null,
198
+ }
199
+ }
200
+
201
+ case 'check_tool': {
202
+ const result = guard.checkTool(String(args.tool_name || ''))
203
+ return {
204
+ allowed: result.allowed,
205
+ level: result.level || null,
206
+ reason: result.reason || null,
207
+ }
208
+ }
209
+
210
+ case 'check_response': {
211
+ const result = guard.checkResponse(String(args.content || ''))
212
+ return {
213
+ canary_leak: result.canaryLeak,
214
+ has_sensitive_data: result.sensitiveData.hasSensitiveData,
215
+ findings: result.sensitiveData.findings.map(f => ({
216
+ type: f.id,
217
+ name: f.name,
218
+ count: f.count,
219
+ })),
220
+ }
221
+ }
222
+
223
+ case 'security_status': {
224
+ return {
225
+ mode: guard.config.mode,
226
+ locale: guard.locale,
227
+ injection_threshold: guard.config.injectionThreshold,
228
+ layers: guard.config.layers,
229
+ capabilities: [
230
+ 'command_safety_check (17 dangerous patterns)',
231
+ 'prompt_injection_detection (32+ rules, zh+en)',
232
+ 'pii_detection (CN ID/phone/bank + global)',
233
+ 'path_protection (12 protected patterns)',
234
+ 'tool_policy (block payment/transfer)',
235
+ 'response_audit (canary + PII)',
236
+ 'data_flow_tracking (DLP)',
237
+ ],
238
+ }
239
+ }
240
+
241
+ default:
242
+ throw new Error(`Unknown tool: ${name}`)
243
+ }
244
+ }
245
+
246
+ // ===== MCP Protocol Handlers =====
247
+
248
+ function handleRequest(req: JsonRpcRequest): JsonRpcResponse | null {
249
+ const { id, method, params } = req
250
+
251
+ switch (method) {
252
+ case 'initialize':
253
+ return {
254
+ jsonrpc: '2.0',
255
+ id: id ?? null,
256
+ result: {
257
+ protocolVersion: '2024-11-05',
258
+ capabilities: { tools: {} },
259
+ serverInfo: {
260
+ name: 'shellward',
261
+ version: pkg.version,
262
+ },
263
+ },
264
+ }
265
+
266
+ case 'notifications/initialized':
267
+ // Client acknowledgement, no response needed
268
+ return null
269
+
270
+ case 'tools/list':
271
+ return {
272
+ jsonrpc: '2.0',
273
+ id: id ?? null,
274
+ result: { tools: TOOLS },
275
+ }
276
+
277
+ case 'resources/list':
278
+ return { jsonrpc: '2.0', id: id ?? null, result: { resources: [] } }
279
+
280
+ case 'prompts/list':
281
+ return { jsonrpc: '2.0', id: id ?? null, result: { prompts: [] } }
282
+
283
+ case 'tools/call': {
284
+ const toolName = (params as any)?.name as string
285
+ const toolArgs = ((params as any)?.arguments || {}) as Record<string, unknown>
286
+
287
+ try {
288
+ const result = executeTool(toolName, toolArgs)
289
+ return {
290
+ jsonrpc: '2.0',
291
+ id: id ?? null,
292
+ result: {
293
+ content: [
294
+ {
295
+ type: 'text',
296
+ text: JSON.stringify(result, null, 2),
297
+ },
298
+ ],
299
+ },
300
+ }
301
+ } catch (err: any) {
302
+ return {
303
+ jsonrpc: '2.0',
304
+ id: id ?? null,
305
+ result: {
306
+ content: [
307
+ {
308
+ type: 'text',
309
+ text: JSON.stringify({ error: err.message }),
310
+ },
311
+ ],
312
+ isError: true,
313
+ },
314
+ }
315
+ }
316
+ }
317
+
318
+ case 'ping':
319
+ return { jsonrpc: '2.0', id: id ?? null, result: {} }
320
+
321
+ default:
322
+ // Unknown methods — return error for requests with id, ignore notifications
323
+ if (id !== undefined) {
324
+ return {
325
+ jsonrpc: '2.0',
326
+ id: id ?? null,
327
+ error: { code: -32601, message: `Method not found: ${method}` },
328
+ }
329
+ }
330
+ return null
331
+ }
332
+ }
333
+
334
+ // ===== Stdio Transport =====
335
+ // Use raw Buffer to handle UTF-8 multi-byte characters correctly.
336
+ // Content-Length is in bytes, not characters.
337
+
338
+ let rawBuffer = Buffer.alloc(0)
339
+
340
+ process.stdin.on('data', (chunk: Buffer) => {
341
+ rawBuffer = Buffer.concat([rawBuffer, chunk])
342
+
343
+ while (true) {
344
+ const headerEnd = rawBuffer.indexOf('\r\n\r\n')
345
+ if (headerEnd === -1) break
346
+
347
+ const header = rawBuffer.slice(0, headerEnd).toString('ascii')
348
+ const lengthMatch = header.match(/Content-Length:\s*(\d+)/i)
349
+ if (!lengthMatch) {
350
+ rawBuffer = rawBuffer.slice(headerEnd + 4)
351
+ continue
352
+ }
353
+
354
+ const contentLength = parseInt(lengthMatch[1], 10)
355
+ const bodyStart = headerEnd + 4
356
+ if (rawBuffer.length < bodyStart + contentLength) break
357
+
358
+ const body = rawBuffer.slice(bodyStart, bodyStart + contentLength).toString('utf8')
359
+ rawBuffer = rawBuffer.slice(bodyStart + contentLength)
360
+
361
+ try {
362
+ const req = JSON.parse(body) as JsonRpcRequest
363
+ const res = handleRequest(req)
364
+ if (res) {
365
+ send(res)
366
+ }
367
+ } catch {
368
+ send({
369
+ jsonrpc: '2.0',
370
+ id: null,
371
+ error: { code: -32700, message: 'Parse error' },
372
+ })
373
+ }
374
+ }
375
+ })
376
+
377
+ function send(msg: JsonRpcResponse) {
378
+ const body = JSON.stringify(msg)
379
+ const header = `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n`
380
+ process.stdout.write(header + body)
381
+ }
382
+
383
+ process.stdin.on('end', () => process.exit(0))
384
+
385
+ // Log to stderr so it doesn't interfere with stdio protocol
386
+ process.stderr.write(`[ShellWard MCP] Server started (mode: ${guard.config.mode}, locale: ${guard.locale})\n`)
@@ -1,6 +1,6 @@
1
1
  // src/rules/dangerous-commands.ts — Shell command blocklist (bilingual)
2
2
 
3
- import type { DangerousCommandRule } from '../types'
3
+ import type { DangerousCommandRule } from '../types.js'
4
4
 
5
5
  export const DANGEROUS_COMMANDS: DangerousCommandRule[] = [
6
6
  {
@@ -1,6 +1,6 @@
1
1
  // src/rules/injection-en.ts — English prompt injection detection rules
2
2
 
3
- import type { InjectionRule } from '../types'
3
+ import type { InjectionRule } from '../types.js'
4
4
 
5
5
  export const INJECTION_RULES_EN: InjectionRule[] = [
6
6
  {
@@ -1,6 +1,6 @@
1
1
  // src/rules/injection-zh.ts — Chinese prompt injection detection rules
2
2
 
3
- import type { InjectionRule } from '../types'
3
+ import type { InjectionRule } from '../types.js'
4
4
 
5
5
  export const INJECTION_RULES_ZH: InjectionRule[] = [
6
6
  {
@@ -1,6 +1,6 @@
1
1
  // src/rules/protected-paths.ts — Paths that should not be written/deleted (bilingual)
2
2
 
3
- import type { ProtectedPathRule } from '../types'
3
+ import type { ProtectedPathRule } from '../types.js'
4
4
 
5
5
  export const PROTECTED_PATHS: ProtectedPathRule[] = [
6
6
  {
@@ -1,6 +1,6 @@
1
1
  // src/rules/sensitive-patterns.ts — PII & secret patterns for output redaction (global + China)
2
2
 
3
- import type { NamedPattern, ScanMatch } from '../types'
3
+ import type { NamedPattern, ScanMatch } from '../types.js'
4
4
 
5
5
  export interface SensitivePattern {
6
6
  id: string
@@ -10,7 +10,7 @@
10
10
  import { get } from 'https'
11
11
  import { mkdirSync, readFileSync, writeFileSync } from 'fs'
12
12
  import { join } from 'path'
13
- import { getHomeDir } from './utils'
13
+ import { getHomeDir } from './utils.js'
14
14
 
15
15
  const CACHE_DIR = join(getHomeDir(), '.openclaw', 'shellward')
16
16
  const CACHE_FILE = join(CACHE_DIR, 'update-cache.json')