shellward 0.5.1 → 0.5.3

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/README.md CHANGED
@@ -50,7 +50,7 @@ Your AI agent has full access to tools — shell, email, HTTP, file system. One
50
50
 
51
51
  | Platform | Integration | Note |
52
52
  |----------|------------|------|
53
- | **OpenClaw** | Plugin | `openclaw plugins install shellward` — out of the box |
53
+ | **OpenClaw** | Plugin + SDK | `openclaw plugins install shellward` — adapts to available hooks |
54
54
  | **Claude Code** | SDK | Anthropic's official CLI agent |
55
55
  | **Cursor** | SDK | AI-powered coding IDE |
56
56
  | **LangChain** | SDK | LLM application framework |
@@ -2,7 +2,7 @@
2
2
  "id": "shellward",
3
3
  "name": "ShellWard",
4
4
  "description": "AI Agent Security Middleware — injection detection, dangerous operation blocking, PII audit (incl. Chinese ID card, phone, bank card), data exfiltration prevention. SDK + OpenClaw plugin.",
5
- "version": "0.5.0",
5
+ "version": "0.5.3",
6
6
  "skills": ["./skills"],
7
7
  "configSchema": {
8
8
  "type": "object",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "shellward",
3
- "version": "0.5.1",
4
- "description": "AI Agent Security Middleware | 身份证/手机号/银行卡 PII 审计 | 中文注入检测 | 8层防御 | SDK + OpenClaw Security layer for AI agents: prompt injection, data leak detection, tool control. Chinese PII audit, 8 defense layers. Zero dependencies.",
3
+ "version": "0.5.3",
4
+ "description": "AI Agent Security Middleware 8-layer defense against prompt injection, data exfiltration & dangerous commands. DLP model: use data freely, block external leaks. Zero dependencies. SDK + OpenClaw plugin. Supports LangChain, AutoGPT, Claude Code, Cursor, OpenAI Agents.",
5
5
  "keywords": [
6
6
  "shellward",
7
7
  "ai-security",
@@ -10,12 +10,17 @@
10
10
  "prompt-injection",
11
11
  "llm-security",
12
12
  "data-protection",
13
+ "data-exfiltration",
14
+ "dlp",
15
+ "guardrails",
16
+ "langchain",
17
+ "autogpt",
18
+ "openai",
19
+ "cursor",
13
20
  "openclaw",
14
- "plugin",
15
21
  "sdk",
16
- "身份证",
17
22
  "PII",
18
- "guardrails"
23
+ "agent-security"
19
24
  ],
20
25
  "author": "jnMetaCode",
21
26
  "license": "Apache-2.0",
package/src/index.ts CHANGED
@@ -20,7 +20,7 @@ import { registerAllCommands } from './commands/index'
20
20
  import { checkForUpdate } from './update-check'
21
21
  import { runAutoCheckOnStartup } from './auto-check'
22
22
 
23
- const CURRENT_VERSION = '0.5.0'
23
+ const CURRENT_VERSION = '0.5.3'
24
24
 
25
25
  // Re-export core engine for SDK usage
26
26
  export { ShellWard } from './core/engine'
@@ -32,11 +32,14 @@ export type { ShellWardConfig } from './types'
32
32
  * If a security hook throws, we log the error and fail-safe:
33
33
  * - before_tool_call: block (deny on error, safer than allow)
34
34
  * - other hooks: return undefined (don't break the chain)
35
+ *
36
+ * Returns boolean indicating whether hook registration succeeded.
37
+ * This allows layers to detect missing hooks and register fallbacks.
35
38
  */
36
39
  function createSafeApi(api: any, guard: ShellWard): any {
37
40
  return {
38
41
  ...api,
39
- on(hookName: string, handler: Function, opts?: any) {
42
+ on(hookName: string, handler: Function, opts?: any): boolean {
40
43
  const isBlockHook = hookName === 'before_tool_call'
41
44
  const wrappedHandler = (event: any) => {
42
45
  try {
@@ -56,7 +59,12 @@ function createSafeApi(api: any, guard: ShellWard): any {
56
59
  return undefined
57
60
  }
58
61
  }
59
- api.on(hookName, wrappedHandler, opts)
62
+ try {
63
+ api.on(hookName, wrappedHandler, opts)
64
+ return true
65
+ } catch {
66
+ return false
67
+ }
60
68
  },
61
69
  }
62
70
  }
@@ -1,10 +1,15 @@
1
1
  // src/layers/data-flow-guard.ts — L7 OpenClaw Adapter
2
- // Thin adapter: wires OpenClaw's after_tool_call + before_tool_call hooks to ShellWard core engine
2
+ // Thin adapter: wires OpenClaw hooks to ShellWard core engine for data flow tracking
3
+ // Compat: uses tool_result_persist as fallback when after_tool_call/before_tool_call unavailable
3
4
 
4
5
  import type { ShellWard } from '../core/engine'
5
6
 
6
7
  export function setupDataFlowGuard(api: any, guard: ShellWard, enforce: boolean) {
7
- api.on('after_tool_call', (event: any) => {
8
+ let hasReadTracker = false
9
+ let hasEgressBlock = false
10
+
11
+ // Primary read tracker: after_tool_call
12
+ hasReadTracker = api.on('after_tool_call', (event: any) => {
8
13
  const toolName = String(event.toolName || '').toLowerCase()
9
14
  const params = (event.params && typeof event.params === 'object') ? event.params : {}
10
15
  const path = String(params.path || params.file_path || params.filename || params.target || '')
@@ -14,7 +19,8 @@ export function setupDataFlowGuard(api: any, guard: ShellWard, enforce: boolean)
14
19
  }
15
20
  }, { name: 'shellward.data-flow-read-tracker', priority: 50 })
16
21
 
17
- api.on('before_tool_call', (event: any) => {
22
+ // Primary egress block: before_tool_call
23
+ hasEgressBlock = api.on('before_tool_call', (event: any) => {
18
24
  const toolName = String(event.toolName || '')
19
25
  const params = (event.params && typeof event.params === 'object') ? event.params : {}
20
26
 
@@ -24,5 +30,61 @@ export function setupDataFlowGuard(api: any, guard: ShellWard, enforce: boolean)
24
30
  }
25
31
  }, { name: 'shellward.data-flow-egress', priority: 250 })
26
32
 
33
+ // Fallback: tool_result_persist for both read tracking and egress detection
34
+ // When after_tool_call/before_tool_call are unavailable, we use tool_result_persist
35
+ // which fires for every tool result before it's persisted to transcript
36
+ if (!hasReadTracker || !hasEgressBlock) {
37
+ api.on('tool_result_persist', (event: any) => {
38
+ const msg = event.message
39
+ if (!msg) return undefined
40
+ const toolName = String(msg.toolName || '')
41
+ if (!toolName) return undefined
42
+ const toolLower = toolName.toLowerCase()
43
+
44
+ // Fallback read tracking: scan tool results for PII to detect sensitive data access
45
+ // The L2 output-scanner already does scanData() which calls markSensitiveData()
46
+ // So read tracking is covered. Here we also track file reads by tool name.
47
+ if (!hasReadTracker && guard.isReadTool(toolLower)) {
48
+ // We don't have the file path from tool_result_persist,
49
+ // but L2's scanData already marks sensitive data when PII is found in results
50
+ guard.log.write({
51
+ level: 'INFO',
52
+ layer: 'L7',
53
+ action: 'detect',
54
+ detail: `Read tool executed: ${toolName} (tracking via result scan)`,
55
+ tool: toolName,
56
+ })
57
+ }
58
+
59
+ // Fallback egress detection: check if an outbound tool was used after sensitive data
60
+ if (!hasEgressBlock) {
61
+ // We can't block here (tool already ran), but we detect and log
62
+ const fakeParams: Record<string, any> = {}
63
+ const result = guard.checkOutbound(toolName, fakeParams)
64
+ if (!result.allowed) {
65
+ guard.log.write({
66
+ level: 'CRITICAL',
67
+ layer: 'L7',
68
+ action: 'detect',
69
+ detail: guard.locale === 'zh'
70
+ ? `⚠️ 数据外泄检测 (无法前置拦截): ${toolName} 在访问敏感数据后执行了外发操作`
71
+ : `⚠️ Data exfiltration detected (pre-block unavailable): ${toolName} sent data after sensitive access`,
72
+ tool: toolName,
73
+ pattern: 'data_exfil_chain',
74
+ })
75
+ }
76
+ }
77
+
78
+ return undefined
79
+ }, { name: 'shellward.data-flow-fallback', priority: 90 })
80
+
81
+ if (!hasReadTracker) {
82
+ api.logger.warn('[ShellWard] L7 Data Flow Guard: after_tool_call unavailable, using result-based tracking')
83
+ }
84
+ if (!hasEgressBlock) {
85
+ api.logger.warn('[ShellWard] L7 Data Flow Guard: before_tool_call unavailable, using post-execution detection')
86
+ }
87
+ }
88
+
27
89
  api.logger.info('[ShellWard] L7 Data Flow Guard enabled')
28
90
  }
@@ -1,9 +1,11 @@
1
1
  // src/layers/input-auditor.ts — L4 OpenClaw Adapter
2
- // Thin adapter: wires OpenClaw's before_tool_call + message_received hooks to ShellWard core engine
2
+ // Thin adapter: wires OpenClaw hooks to ShellWard core engine for injection detection
3
+ // Compat: supports both old (message_received) and new (message:received) hook names
3
4
 
4
5
  import type { ShellWard } from '../core/engine'
5
6
 
6
7
  export function setupInputAuditor(api: any, guard: ShellWard, enforce: boolean) {
8
+ // Tool call parameter scanning via before_tool_call
7
9
  api.on('before_tool_call', (event: any) => {
8
10
  const args: Record<string, any> = (event.params && typeof event.params === 'object') ? event.params : {}
9
11
  const texts = guard.extractTextFields(args)
@@ -22,11 +24,18 @@ export function setupInputAuditor(api: any, guard: ShellWard, enforce: boolean)
22
24
  }
23
25
  }, { name: 'shellward.input-auditor', priority: 300 })
24
26
 
25
- api.on('message_received', (event: any) => {
27
+ // Message scanning: try both OpenClaw hook naming conventions
28
+ const messageHandler = (event: any) => {
26
29
  const content = typeof event.content === 'string' ? event.content : ''
27
30
  if (!content) return
28
31
  guard.checkInjection(content, { source: 'message' })
29
- }, { name: 'shellward.message-auditor', priority: 100 })
32
+ }
33
+
34
+ // Try new-style colon-separated hook name first, then legacy underscore style
35
+ const registered = api.on('message:received', messageHandler, { name: 'shellward.message-auditor', priority: 100 })
36
+ if (!registered) {
37
+ api.on('message_received', messageHandler, { name: 'shellward.message-auditor', priority: 100 })
38
+ }
30
39
 
31
40
  api.logger.info(`[ShellWard] L4 Input Auditor enabled`)
32
41
  }
@@ -1,10 +1,11 @@
1
1
  // src/layers/outbound-guard.ts — L6 OpenClaw Adapter
2
- // Thin adapter: wires OpenClaw's message_sending hook to ShellWard core engine
2
+ // Thin adapter: wires OpenClaw hooks to ShellWard core engine for outbound response scanning
3
+ // Compat: supports both old (message_sending) and new (message:sent) hook names
3
4
 
4
5
  import type { ShellWard } from '../core/engine'
5
6
 
6
7
  export function setupOutboundGuard(api: any, guard: ShellWard, enforce: boolean) {
7
- api.on('message_sending', (event: any) => {
8
+ const handler = (event: any) => {
8
9
  const content = event.content
9
10
  if (!content || typeof content !== 'string') return undefined
10
11
 
@@ -18,7 +19,13 @@ export function setupOutboundGuard(api: any, guard: ShellWard, enforce: boolean)
18
19
  }
19
20
 
20
21
  return undefined
21
- }, { name: 'shellward.outbound-guard', priority: 100 })
22
+ }
23
+
24
+ // Try new-style hook name first, then legacy
25
+ const registered = api.on('message:sent', handler, { name: 'shellward.outbound-guard', priority: 100 })
26
+ if (!registered) {
27
+ api.on('message_sending', handler, { name: 'shellward.outbound-guard', priority: 100 })
28
+ }
22
29
 
23
30
  api.logger.info('[ShellWard] L6 Outbound Guard enabled')
24
31
  }
@@ -1,10 +1,12 @@
1
1
  // src/layers/session-guard.ts — L8 OpenClaw Adapter
2
- // Thin adapter: wires OpenClaw's session_end + subagent_spawning hooks to ShellWard core engine
2
+ // Thin adapter: wires OpenClaw hooks to ShellWard core engine for session monitoring
3
+ // Compat: supports multiple hook naming conventions and graceful degradation
3
4
 
4
5
  import type { ShellWard } from '../core/engine'
5
6
 
6
7
  export function setupSessionGuard(api: any, guard: ShellWard, enforce: boolean) {
7
- api.on('session_end', () => {
8
+ // Session end: try new-style, then legacy, then command-based
9
+ const sessionEndHandler = () => {
8
10
  guard.log.write({
9
11
  level: 'INFO',
10
12
  layer: 'L8',
@@ -13,9 +15,19 @@ export function setupSessionGuard(api: any, guard: ShellWard, enforce: boolean)
13
15
  ? '会话结束 — 安全审计完成'
14
16
  : 'Session ended — security audit complete',
15
17
  })
16
- }, { name: 'shellward.session-end', priority: 50 })
18
+ }
17
19
 
18
- api.on('subagent_spawning', (event: any) => {
20
+ let registered = api.on('session:end', sessionEndHandler, { name: 'shellward.session-end', priority: 50 })
21
+ if (!registered) {
22
+ registered = api.on('session_end', sessionEndHandler, { name: 'shellward.session-end', priority: 50 })
23
+ }
24
+ if (!registered) {
25
+ // Fallback: listen for command:new (session reset) as a proxy
26
+ api.on('command:new', sessionEndHandler, { name: 'shellward.session-end-fallback', priority: 50 })
27
+ }
28
+
29
+ // Subagent monitoring: try multiple naming conventions
30
+ const subagentHandler = (event: any) => {
19
31
  const mode = event.mode || 'unknown'
20
32
  guard.log.write({
21
33
  level: 'MEDIUM',
@@ -25,7 +37,16 @@ export function setupSessionGuard(api: any, guard: ShellWard, enforce: boolean)
25
37
  ? `子 Agent 创建: mode=${mode}, agentId=${event.agentId || 'unknown'}`
26
38
  : `Subagent spawning: mode=${mode}, agentId=${event.agentId || 'unknown'}`,
27
39
  })
28
- }, { name: 'shellward.subagent-guard', priority: 100 })
40
+ }
41
+
42
+ // Try ContextEngine-style hook, then legacy
43
+ let subRegistered = api.on('subagent:spawning', subagentHandler, { name: 'shellward.subagent-guard', priority: 100 })
44
+ if (!subRegistered) {
45
+ subRegistered = api.on('subagent_spawning', subagentHandler, { name: 'shellward.subagent-guard', priority: 100 })
46
+ }
47
+ if (!subRegistered) {
48
+ api.logger.warn('[ShellWard] L8 Session Guard: subagent hooks unavailable, subagent monitoring disabled')
49
+ }
29
50
 
30
51
  api.logger.info('[ShellWard] L8 Session Guard enabled')
31
52
  }
@@ -1,10 +1,12 @@
1
1
  // src/layers/tool-blocker.ts — L3 OpenClaw Adapter
2
2
  // Thin adapter: wires OpenClaw's before_tool_call hook to ShellWard core engine
3
+ // Compat: falls back to tool_result_persist for post-execution detection if before_tool_call unavailable
3
4
 
4
5
  import type { ShellWard } from '../core/engine'
5
6
 
6
7
  export function setupToolBlocker(api: any, guard: ShellWard, enforce: boolean) {
7
- api.on('before_tool_call', (event: any) => {
8
+ // Primary: pre-execution blocking via before_tool_call
9
+ const hasBeforeToolCall = api.on('before_tool_call', (event: any) => {
8
10
  const tool = String(event.toolName || '')
9
11
  const args: Record<string, any> = (event.params && typeof event.params === 'object') ? event.params : {}
10
12
 
@@ -31,5 +33,35 @@ export function setupToolBlocker(api: any, guard: ShellWard, enforce: boolean) {
31
33
  }
32
34
  }, { name: 'shellward.tool-blocker', priority: 200 })
33
35
 
36
+ // Fallback: post-execution detection via tool_result_persist
37
+ // When before_tool_call is unavailable (some OpenClaw versions), we still detect
38
+ // dangerous tool usage after the fact and log it for audit trail
39
+ if (!hasBeforeToolCall) {
40
+ api.on('tool_result_persist', (event: any) => {
41
+ const msg = event.message
42
+ if (!msg) return undefined
43
+ const tool = String(msg.toolName || '')
44
+ if (!tool) return undefined
45
+
46
+ // Check if the tool itself is blocked
47
+ const toolCheck = guard.checkTool(tool)
48
+ if (!toolCheck.allowed) {
49
+ guard.log.write({
50
+ level: 'CRITICAL',
51
+ layer: 'L3',
52
+ action: 'detect',
53
+ detail: guard.locale === 'zh'
54
+ ? `⚠️ 高危工具已执行 (无法前置拦截): ${tool} — ${toolCheck.reason}`
55
+ : `⚠️ Dangerous tool executed (pre-block unavailable): ${tool} — ${toolCheck.reason}`,
56
+ tool,
57
+ })
58
+ }
59
+
60
+ return undefined
61
+ }, { name: 'shellward.tool-blocker-fallback', priority: 190 })
62
+
63
+ api.logger.warn('[ShellWard] L3 Tool Blocker: before_tool_call hook unavailable, using post-execution detection')
64
+ }
65
+
34
66
  api.logger.info('[ShellWard] L3 Tool Blocker enabled')
35
67
  }