clawmoat 0.8.0 → 1.0.0
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/.dockerignore +9 -0
- package/CHANGELOG.md +18 -0
- package/DEMO.md +87 -0
- package/Dockerfile +5 -18
- package/README.md +232 -8
- package/THREAT_MODEL.md +129 -0
- package/agent/README.md +131 -0
- package/agent/index.js +471 -0
- package/agent/install-service.sh +94 -0
- package/agent/openclaw-hook.js +453 -0
- package/agent/provider-setup.js +649 -0
- package/agent/setup.js +274 -0
- package/assets/BADGE-USAGE.md +20 -0
- package/assets/clawmoat-badge.svg +21 -0
- package/bin/clawmoat.js +468 -111
- package/docs/affiliates/dashboard.html +124 -0
- package/docs/affiliates/index.html +236 -0
- package/docs/agent-install.html +183 -0
- package/docs/ai-agent-security-scanner.html +10 -6
- package/docs/badge/index.html +149 -0
- package/docs/badge/scanning.svg +23 -0
- package/docs/blog/386-malicious-skills.html +11 -4
- package/docs/blog/40000-exposed-openclaw-instances.html +11 -4
- package/docs/blog/agent-trust-protocol.html +5 -4
- package/docs/blog/ai-agent-earns-commissions.html +230 -0
- package/docs/blog/bugmageddon-agent-firewall.html +174 -0
- package/docs/blog/calculator-math.html +180 -0
- package/docs/blog/clawmoat-vs-llamafirewall-nemo-guardrails.html +10 -4
- package/docs/blog/host-guardian-launch.html +18 -8
- package/docs/blog/ibm-experts-agent-runtime-protection.html +15 -6
- package/docs/blog/index.html +67 -9
- package/docs/blog/langchain-security-tutorial.html +18 -8
- package/docs/blog/mcp-30-cves-security-crisis.html +11 -4
- package/docs/blog/meta-researcher-rogue-agent.html +201 -0
- package/docs/blog/microsoft-openclaw-workstation-security.html +5 -4
- package/docs/blog/nist-ai-agent-standards-clawmoat.html +16 -8
- package/docs/blog/oasis-websocket-hijack.html +11 -4
- package/docs/blog/ollama-openclaw-security.html +10 -4
- package/docs/blog/openclaw-enterprise-readiness-claw10.html +5 -4
- package/docs/blog/openclaw-security-reckoning-2026.html +11 -4
- package/docs/blog/owasp-agentic-ai-top10.html +18 -8
- package/docs/blog/securing-ai-agents.html +18 -8
- package/docs/blog/supply-chain-agents.html +18 -8
- package/docs/business/index.html +11 -16
- package/docs/business/install.html +21 -7
- package/docs/checklist.html +10 -4
- package/docs/compare/index.html +122 -0
- package/docs/compare/lakera/index.html +62 -0
- package/docs/compare/llm-guard/index.html +49 -0
- package/docs/compare/snyk-agent-scan/index.html +63 -0
- package/docs/compare.html +10 -6
- package/docs/dashboard/index.html +520 -0
- package/docs/finance/index.html +9 -6
- package/docs/guides/business-deployment.html +770 -0
- package/docs/hall-of-fame.html +11 -5
- package/docs/index.html +266 -137
- package/docs/integrations/langchain.html +14 -6
- package/docs/integrations/openai.html +14 -6
- package/docs/integrations/openclaw.html +55 -7
- package/docs/plans/2026-03-26-threat-intel-api.md +255 -0
- package/docs/plans/2026-04-14-bugmageddon-marketing-pack.md +329 -0
- package/docs/plans/2026-04-14-clawmoat-v1-bugmageddon.md +248 -0
- package/docs/plans/2026-04-14-v1-release-update.md +91 -0
- package/docs/plans/2026-04-19-supabase-audit.md +68 -0
- package/docs/plans/2026-05-12-sales-push.md +303 -0
- package/docs/playground/index.html +893 -0
- package/docs/playground.html +4 -7
- package/docs/rfcs/defense-in-depth.md +467 -0
- package/docs/scan/index.html +156 -12
- package/docs/services/case-study.html +255 -0
- package/docs/services/downloads/install-openclaw.bat +45 -0
- package/docs/services/downloads/install-openclaw.command +38 -0
- package/docs/services/downloads/install-openclaw.sh +38 -0
- package/docs/services/get-started.html +165 -0
- package/docs/services/index.html +598 -0
- package/docs/services/multi-agent-security.html +284 -0
- package/docs/services/one-pager.html +99 -0
- package/docs/services/pitch-deck.html +229 -0
- package/docs/services/roi-calculator.html +258 -0
- package/docs/sitemap.xml +62 -2
- package/docs/support/index.html +12 -1
- package/docs/templates/customer-service/HEARTBEAT.md +61 -0
- package/docs/templates/customer-service/MEMORY.md +89 -0
- package/docs/templates/customer-service/SOUL.md +41 -0
- package/docs/templates/customer-service/USER.md +56 -0
- package/docs/templates/executive/HEARTBEAT.md +86 -0
- package/docs/templates/executive/MEMORY.md +92 -0
- package/docs/templates/executive/SOUL.md +44 -0
- package/docs/templates/executive/USER.md +62 -0
- package/docs/templates/finance/HEARTBEAT.md +58 -0
- package/docs/templates/finance/MEMORY.md +87 -0
- package/docs/templates/finance/SOUL.md +38 -0
- package/docs/templates/finance/USER.md +53 -0
- package/docs/templates/index.html +115 -0
- package/docs/templates/operations/HEARTBEAT.md +63 -0
- package/docs/templates/operations/MEMORY.md +68 -0
- package/docs/templates/operations/SOUL.md +38 -0
- package/docs/templates/operations/USER.md +49 -0
- package/docs/templates/sales/HEARTBEAT.md +55 -0
- package/docs/templates/sales/MEMORY.md +89 -0
- package/docs/templates/sales/SOUL.md +34 -0
- package/docs/templates/sales/USER.md +54 -0
- package/eslint.config.js +32 -0
- package/evals/README.md +29 -0
- package/evals/cases.json +390 -0
- package/evals/results.md +68 -0
- package/evals/run.js +180 -0
- package/examples/demo-attack/demo.js +186 -0
- package/examples/python-quickstart/README.md +54 -0
- package/examples/python-quickstart/clawmoat_client.py +167 -0
- package/examples/video-demo/README.md +14 -0
- package/examples/video-demo/scene-a-normal.js +29 -0
- package/examples/video-demo/scene-b-attack-arrives.js +31 -0
- package/examples/video-demo/scene-c-hijack.js +44 -0
- package/examples/video-demo/scene-d-clawmoat.js +46 -0
- package/integrations/crewai/README.md +32 -0
- package/integrations/crewai/clawmoat_crewai/__init__.py +17 -0
- package/integrations/crewai/clawmoat_crewai/guard.py +103 -0
- package/integrations/crewai/pyproject.toml +21 -0
- package/integrations/langchain/README.md +91 -0
- package/integrations/langchain/clawmoat_langchain/__init__.py +17 -0
- package/integrations/langchain/clawmoat_langchain/callback.py +489 -0
- package/integrations/langchain/pyproject.toml +32 -0
- package/integrations/litellm/README.md +324 -0
- package/integrations/litellm/clawmoat_litellm/__init__.py +21 -0
- package/integrations/litellm/clawmoat_litellm/callback.py +329 -0
- package/integrations/litellm/clawmoat_litellm/proxy_middleware.py +224 -0
- package/integrations/litellm/pyproject.toml +74 -0
- package/integrations/openai-agents/README.md +392 -0
- package/integrations/openai-agents/clawmoat_openai_agents/__init__.py +20 -0
- package/integrations/openai-agents/clawmoat_openai_agents/guardrail.py +431 -0
- package/integrations/openai-agents/clawmoat_openai_agents/middleware.py +311 -0
- package/integrations/openai-agents/pyproject.toml +76 -0
- package/package.json +6 -5
- package/plugins/openclaw-adapter/PHASE1.md +439 -0
- package/plugins/openclaw-adapter/README.md +103 -0
- package/plugins/openclaw-adapter/SPEC.md +1644 -0
- package/plugins/openclaw-adapter/package.json +31 -0
- package/plugins/openclaw-adapter/src/index.test.ts +226 -0
- package/plugins/openclaw-adapter/src/index.ts +140 -0
- package/plugins/openclaw-adapter/tsconfig.json +14 -0
- package/server/data/threats.json +290 -0
- package/server/index.js +142 -7
- package/src/adapters/express.js +161 -0
- package/src/adapters/index.js +92 -0
- package/src/adapters/langchain.js +185 -0
- package/src/approval/index.js +456 -0
- package/src/ban-scanner.js +200 -0
- package/src/boundary-scanner.js +296 -0
- package/src/ci-scanner.js +279 -0
- package/src/code-scanner.js +245 -0
- package/src/enforce.js +166 -0
- package/src/formatters/json.js +80 -0
- package/src/formatters/sarif.js +388 -0
- package/src/guardian/alerts.js +34 -3
- package/src/guardian/index.js +41 -2
- package/src/index.js +102 -0
- package/src/integrations/agentmesh.js +501 -0
- package/src/language-detector.js +201 -0
- package/src/mcp-scanner.js +253 -0
- package/src/multimodal/index.js +579 -0
- package/src/obfuscation-scanner.js +457 -0
- package/src/policy-engine.js +402 -0
- package/src/scanners/dependency-attacks.js +128 -0
- package/src/scanners/prompt-injection.js +18 -0
- package/src/scanners/supply-chain.js +14 -0
- package/src/templates/default-config.yml +90 -0
- package/src/vuln-ops/exploitability.js +46 -0
- package/src/watch/live-monitor.js +720 -0
- package/clawmoat-0.8.0.tgz +0 -0
- package/server/index.js.patch +0 -1
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ClawMoat OpenClaw Hook Monitor
|
|
4
|
+
*
|
|
5
|
+
* Watches ~/.openclaw/ for real-time session activity:
|
|
6
|
+
* - Tails session JSONL files for inbound/outbound messages
|
|
7
|
+
* - Scans each message through ClawMoat
|
|
8
|
+
* - Logs threats to audit log and optionally reports to cloud
|
|
9
|
+
* - Can be run standalone or imported by index.js
|
|
10
|
+
*
|
|
11
|
+
* Usage: node agent/openclaw-hook.js [--verbose] [--dry-run]
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
|
|
20
|
+
// ─── Paths ────────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw');
|
|
23
|
+
const SESSIONS_DIR = path.join(OPENCLAW_DIR, 'agents', 'main', 'sessions');
|
|
24
|
+
const DELIVERY_QUEUE_DIR = path.join(OPENCLAW_DIR, 'delivery-queue');
|
|
25
|
+
const HOOKS_DIR = path.join(OPENCLAW_DIR, 'hooks');
|
|
26
|
+
const LOGS_DIR = path.join(OPENCLAW_DIR, 'logs');
|
|
27
|
+
|
|
28
|
+
const args = process.argv.slice(2);
|
|
29
|
+
const VERBOSE = args.includes('--verbose') || args.includes('-v');
|
|
30
|
+
const DRY_RUN = args.includes('--dry-run');
|
|
31
|
+
|
|
32
|
+
// ─── Logger ───────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
function ts() { return new Date().toISOString(); }
|
|
35
|
+
function log(msg, ...rest) {
|
|
36
|
+
if (VERBOSE) console.log(`${ts()} [hook]`, msg, ...rest);
|
|
37
|
+
}
|
|
38
|
+
function warn(msg, ...rest) {
|
|
39
|
+
console.warn(`${ts()} [hook] WARN`, msg, ...rest);
|
|
40
|
+
}
|
|
41
|
+
function threat(msg, ...rest) {
|
|
42
|
+
console.log(`${ts()} [hook] 🚨 THREAT`, msg, ...rest);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── ClawMoat Scanner ─────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
function loadScanner() {
|
|
48
|
+
try {
|
|
49
|
+
const ClawMoat = require(path.join(os.homedir(), 'clawmoat', 'src', 'index.js'));
|
|
50
|
+
const moat = new ClawMoat({ quiet: true });
|
|
51
|
+
log('Scanner loaded from ~/clawmoat');
|
|
52
|
+
return moat;
|
|
53
|
+
} catch (e) {
|
|
54
|
+
try {
|
|
55
|
+
const { ClawMoat } = require('clawmoat');
|
|
56
|
+
const moat = new ClawMoat({ quiet: true });
|
|
57
|
+
log('Scanner loaded from npm');
|
|
58
|
+
return moat;
|
|
59
|
+
} catch (e2) {
|
|
60
|
+
throw new Error(`Cannot load ClawMoat: ${e.message}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── Content Extractor ────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Extract scannable text from an OpenClaw session entry.
|
|
69
|
+
* Returns null if nothing to scan.
|
|
70
|
+
*/
|
|
71
|
+
function extractContent(entry) {
|
|
72
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
73
|
+
|
|
74
|
+
// Standard message event
|
|
75
|
+
if (entry.type === 'message' && entry.message) {
|
|
76
|
+
const msg = entry.message;
|
|
77
|
+
const role = msg.role;
|
|
78
|
+
const texts = [];
|
|
79
|
+
|
|
80
|
+
if (Array.isArray(msg.content)) {
|
|
81
|
+
for (const block of msg.content) {
|
|
82
|
+
if (block.type === 'text' && block.text) {
|
|
83
|
+
texts.push(block.text);
|
|
84
|
+
}
|
|
85
|
+
// Tool use input (scanToolCalls)
|
|
86
|
+
if (block.type === 'tool_use' && block.input) {
|
|
87
|
+
const inputStr = typeof block.input === 'string'
|
|
88
|
+
? block.input
|
|
89
|
+
: JSON.stringify(block.input);
|
|
90
|
+
texts.push(`[tool:${block.name}] ${inputStr}`);
|
|
91
|
+
}
|
|
92
|
+
// Tool result
|
|
93
|
+
if (block.type === 'tool_result') {
|
|
94
|
+
const content = Array.isArray(block.content)
|
|
95
|
+
? block.content.filter(c => c.type === 'text').map(c => c.text).join('\n')
|
|
96
|
+
: block.content;
|
|
97
|
+
if (content) texts.push(`[tool_result] ${content}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} else if (typeof msg.content === 'string') {
|
|
101
|
+
texts.push(msg.content);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (texts.length === 0) return null;
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
text: texts.join('\n'),
|
|
108
|
+
role,
|
|
109
|
+
direction: role === 'user' ? 'inbound' : 'outbound',
|
|
110
|
+
type: 'message',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Delivery queue item (inbound from Telegram/Discord)
|
|
115
|
+
if (entry.type === 'delivery' || entry.channel) {
|
|
116
|
+
const text = entry.text || entry.message || entry.content;
|
|
117
|
+
if (!text) return null;
|
|
118
|
+
return {
|
|
119
|
+
text: String(text),
|
|
120
|
+
role: 'user',
|
|
121
|
+
direction: 'inbound',
|
|
122
|
+
type: 'delivery',
|
|
123
|
+
channel: entry.channel,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─── Threat Handler ───────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Handle a detected threat.
|
|
134
|
+
* @param {Object} result - ClawMoat scan result
|
|
135
|
+
* @param {Object} content - Extracted content
|
|
136
|
+
* @param {Object} context - File/session context
|
|
137
|
+
* @param {Function} [onThreat] - Optional callback for blocking/alerting
|
|
138
|
+
*/
|
|
139
|
+
async function handleThreat(result, content, context, onThreat) {
|
|
140
|
+
const findings = result.findings
|
|
141
|
+
.map(f => `${f.type}(${f.severity}): ${f.subtype || ''}${f.matched ? ` — "${f.matched}"` : ''}`)
|
|
142
|
+
.join('; ');
|
|
143
|
+
|
|
144
|
+
threat(`[${context.sessionId}] ${content.direction} [${content.role}]`);
|
|
145
|
+
threat(` Severity: ${result.severity} | Action: ${result.action}`);
|
|
146
|
+
threat(` Findings: ${findings}`);
|
|
147
|
+
threat(` Text: "${content.text.slice(0, 120).replace(/\n/g, ' ')}"`);
|
|
148
|
+
|
|
149
|
+
if (onThreat) {
|
|
150
|
+
await onThreat({ result, content, context });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─── Session File Tailer ──────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
class SessionTailer {
|
|
157
|
+
constructor(sessionsDir, scanner, handlers = {}) {
|
|
158
|
+
this.dir = sessionsDir;
|
|
159
|
+
this.scanner = scanner;
|
|
160
|
+
this.handlers = handlers;
|
|
161
|
+
this.files = new Map(); // path → { position, watcher }
|
|
162
|
+
this.dirWatcher = null;
|
|
163
|
+
this.stats = { processed: 0, threats: 0, errors: 0 };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
start() {
|
|
167
|
+
if (!fs.existsSync(this.dir)) {
|
|
168
|
+
warn(`Sessions dir not found: ${this.dir}`);
|
|
169
|
+
// Watch for directory creation
|
|
170
|
+
this._watchForDir();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
this._init();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
_watchForDir() {
|
|
177
|
+
const grandparent = path.join(this.dir, '..', '..');
|
|
178
|
+
if (!fs.existsSync(grandparent)) return;
|
|
179
|
+
const w = fs.watch(grandparent, { recursive: true }, (event, filename) => {
|
|
180
|
+
if (filename && filename.includes('sessions') && fs.existsSync(this.dir)) {
|
|
181
|
+
w.close();
|
|
182
|
+
this._init();
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
_init() {
|
|
188
|
+
log(`Watching sessions dir: ${this.dir}`);
|
|
189
|
+
|
|
190
|
+
// Watch existing session files (start at EOF — don't replay history)
|
|
191
|
+
try {
|
|
192
|
+
const files = fs.readdirSync(this.dir).filter(f => f.endsWith('.jsonl'));
|
|
193
|
+
for (const f of files) {
|
|
194
|
+
this._addFile(path.join(this.dir, f), /* startAtEnd */ true);
|
|
195
|
+
}
|
|
196
|
+
log(`Watching ${files.length} existing session file(s)`);
|
|
197
|
+
} catch (e) {
|
|
198
|
+
warn(`Cannot read sessions dir: ${e.message}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Watch for new session files
|
|
202
|
+
this.dirWatcher = fs.watch(this.dir, (event, filename) => {
|
|
203
|
+
if (!filename?.endsWith('.jsonl')) return;
|
|
204
|
+
const fp = path.join(this.dir, filename);
|
|
205
|
+
if (!this.files.has(fp) && fs.existsSync(fp)) {
|
|
206
|
+
log(`New session: ${filename}`);
|
|
207
|
+
this._addFile(fp, /* startAtEnd */ false);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
_addFile(filepath, startAtEnd = true) {
|
|
213
|
+
if (this.files.has(filepath)) return;
|
|
214
|
+
|
|
215
|
+
let position = 0;
|
|
216
|
+
if (startAtEnd) {
|
|
217
|
+
try { position = fs.statSync(filepath).size; } catch {}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const state = { position, partial: '' };
|
|
221
|
+
this.files.set(filepath, state);
|
|
222
|
+
|
|
223
|
+
const w = fs.watch(filepath, (event) => {
|
|
224
|
+
if (event === 'change') this._drain(filepath, state);
|
|
225
|
+
});
|
|
226
|
+
w.on('error', () => this.files.delete(filepath));
|
|
227
|
+
state.watcher = w;
|
|
228
|
+
|
|
229
|
+
log(`Tailing: ${path.basename(filepath)} (offset ${position})`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
_drain(filepath, state) {
|
|
233
|
+
let fd;
|
|
234
|
+
try {
|
|
235
|
+
fd = fs.openSync(filepath, 'r');
|
|
236
|
+
const stat = fs.fstatSync(fd);
|
|
237
|
+
if (stat.size <= state.position) return;
|
|
238
|
+
|
|
239
|
+
const toRead = Math.min(stat.size - state.position, 131072); // 128KB max per read
|
|
240
|
+
const buf = Buffer.alloc(toRead);
|
|
241
|
+
const n = fs.readSync(fd, buf, 0, toRead, state.position);
|
|
242
|
+
state.position += n;
|
|
243
|
+
|
|
244
|
+
const chunk = state.partial + buf.slice(0, n).toString('utf8');
|
|
245
|
+
const lines = chunk.split('\n');
|
|
246
|
+
state.partial = lines.pop();
|
|
247
|
+
|
|
248
|
+
for (const line of lines) {
|
|
249
|
+
const trimmed = line.trim();
|
|
250
|
+
if (!trimmed) continue;
|
|
251
|
+
try {
|
|
252
|
+
const entry = JSON.parse(trimmed);
|
|
253
|
+
this._processEntry(entry, filepath);
|
|
254
|
+
} catch {}
|
|
255
|
+
}
|
|
256
|
+
} catch (e) {
|
|
257
|
+
warn(`Read error ${path.basename(filepath)}: ${e.message}`);
|
|
258
|
+
} finally {
|
|
259
|
+
if (fd !== undefined) try { fs.closeSync(fd); } catch {}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async _processEntry(entry, filepath) {
|
|
264
|
+
const content = extractContent(entry);
|
|
265
|
+
if (!content) return;
|
|
266
|
+
|
|
267
|
+
const sessionId = path.basename(filepath, '.jsonl').slice(0, 8);
|
|
268
|
+
const context = { sessionId, filepath, entryId: entry.id, timestamp: entry.timestamp };
|
|
269
|
+
|
|
270
|
+
this.stats.processed++;
|
|
271
|
+
|
|
272
|
+
let result;
|
|
273
|
+
try {
|
|
274
|
+
if (content.direction === 'inbound') {
|
|
275
|
+
result = this.scanner.scanInbound(content.text);
|
|
276
|
+
} else {
|
|
277
|
+
result = this.scanner.scanOutbound(content.text);
|
|
278
|
+
}
|
|
279
|
+
} catch (e) {
|
|
280
|
+
this.stats.errors++;
|
|
281
|
+
warn(`Scan error: ${e.message}`);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (VERBOSE && result.safe) {
|
|
286
|
+
log(`CLEAN [${sessionId}] ${content.direction} "${content.text.slice(0, 60).replace(/\n/g, ' ')}"`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (!result.safe) {
|
|
290
|
+
this.stats.threats++;
|
|
291
|
+
await handleThreat(result, content, context, this.handlers.onThreat);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Always emit for audit logging
|
|
295
|
+
if (this.handlers.onScan) {
|
|
296
|
+
this.handlers.onScan({ result, content, context });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
stop() {
|
|
301
|
+
if (this.dirWatcher) this.dirWatcher.close();
|
|
302
|
+
for (const [, state] of this.files) {
|
|
303
|
+
if (state.watcher) state.watcher.close();
|
|
304
|
+
}
|
|
305
|
+
this.files.clear();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
getStats() { return { ...this.stats }; }
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ─── Delivery Queue Watcher ───────────────────────────────────────────────────
|
|
312
|
+
// Watch the delivery queue for incoming channel messages (Telegram, Discord, etc.)
|
|
313
|
+
|
|
314
|
+
class DeliveryWatcher {
|
|
315
|
+
constructor(queueDir, scanner, handlers = {}) {
|
|
316
|
+
this.dir = queueDir;
|
|
317
|
+
this.scanner = scanner;
|
|
318
|
+
this.handlers = handlers;
|
|
319
|
+
this.watcher = null;
|
|
320
|
+
this.stats = { processed: 0, threats: 0 };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
start() {
|
|
324
|
+
if (!fs.existsSync(this.dir)) {
|
|
325
|
+
log(`Delivery queue dir not found: ${this.dir} (skipping)`);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
log(`Watching delivery queue: ${this.dir}`);
|
|
329
|
+
this.watcher = fs.watch(this.dir, { recursive: false }, async (event, filename) => {
|
|
330
|
+
if (!filename || !filename.endsWith('.json')) return;
|
|
331
|
+
const fp = path.join(this.dir, filename);
|
|
332
|
+
if (!fs.existsSync(fp)) return;
|
|
333
|
+
await this._processFile(fp);
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async _processFile(filepath) {
|
|
338
|
+
let data;
|
|
339
|
+
try {
|
|
340
|
+
data = JSON.parse(fs.readFileSync(filepath, 'utf8'));
|
|
341
|
+
} catch { return; }
|
|
342
|
+
|
|
343
|
+
const text = data.text || data.message || data.content;
|
|
344
|
+
if (!text) return;
|
|
345
|
+
|
|
346
|
+
this.stats.processed++;
|
|
347
|
+
|
|
348
|
+
let result;
|
|
349
|
+
try {
|
|
350
|
+
result = this.scanner.scanInbound(String(text));
|
|
351
|
+
} catch (e) {
|
|
352
|
+
warn(`Delivery scan error: ${e.message}`);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (!result.safe) {
|
|
357
|
+
this.stats.threats++;
|
|
358
|
+
const content = { text: String(text), role: 'user', direction: 'inbound', type: 'delivery', channel: data.channel };
|
|
359
|
+
const context = { sessionId: 'delivery', filepath };
|
|
360
|
+
await handleThreat(result, content, context, this.handlers.onThreat);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (this.handlers.onScan) {
|
|
364
|
+
this.handlers.onScan({ result, content: { text, direction: 'inbound' }, context: { filepath } });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
stop() {
|
|
369
|
+
if (this.watcher) this.watcher.close();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ─── Standalone Mode ──────────────────────────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
if (require.main === module) {
|
|
376
|
+
const CONFIG_PATH = path.join(os.homedir(), '.clawmoat', 'agent.json');
|
|
377
|
+
let config = {};
|
|
378
|
+
try {
|
|
379
|
+
config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
380
|
+
} catch {
|
|
381
|
+
console.log('No config found — running with defaults (no cloud reporting)');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const AUDIT_PATH = (config.auditLog || '~/.clawmoat/audit.log').replace(/^~/, os.homedir());
|
|
385
|
+
fs.mkdirSync(path.dirname(AUDIT_PATH), { recursive: true });
|
|
386
|
+
const auditStream = fs.createWriteStream(AUDIT_PATH, { flags: 'a' });
|
|
387
|
+
|
|
388
|
+
let scanner;
|
|
389
|
+
try {
|
|
390
|
+
scanner = loadScanner();
|
|
391
|
+
} catch (e) {
|
|
392
|
+
console.error('FATAL:', e.message);
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const handlers = {
|
|
397
|
+
onScan: ({ result, content, context }) => {
|
|
398
|
+
const entry = {
|
|
399
|
+
ts: new Date().toISOString(),
|
|
400
|
+
sessionId: context.sessionId,
|
|
401
|
+
direction: content.direction,
|
|
402
|
+
role: content.role,
|
|
403
|
+
safe: result.safe,
|
|
404
|
+
severity: result.severity,
|
|
405
|
+
action: result.action,
|
|
406
|
+
findings: result.findings?.length || 0,
|
|
407
|
+
textPreview: content.text.slice(0, 200),
|
|
408
|
+
};
|
|
409
|
+
auditStream.write(JSON.stringify(entry) + '\n');
|
|
410
|
+
},
|
|
411
|
+
onThreat: async ({ result, content, context }) => {
|
|
412
|
+
// Cloud reporting if configured
|
|
413
|
+
if (config.reportToCloud && config.apiKey && config.apiKey !== 'cm_live_...') {
|
|
414
|
+
const { reportToCloud } = require('./index.js');
|
|
415
|
+
// Use the main agent's reporting if available, else fire-and-forget
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const tailer = new SessionTailer(SESSIONS_DIR, scanner, handlers);
|
|
421
|
+
const deliveryWatcher = new DeliveryWatcher(DELIVERY_QUEUE_DIR, scanner, handlers);
|
|
422
|
+
|
|
423
|
+
tailer.start();
|
|
424
|
+
deliveryWatcher.start();
|
|
425
|
+
|
|
426
|
+
console.log(`${ts()} [hook] ClawMoat OpenClaw Hook running`);
|
|
427
|
+
console.log(`${ts()} [hook] Monitoring: ${SESSIONS_DIR}`);
|
|
428
|
+
console.log(`${ts()} [hook] Audit log: ${AUDIT_PATH}`);
|
|
429
|
+
if (VERBOSE) console.log(`${ts()} [hook] Verbose mode on`);
|
|
430
|
+
if (DRY_RUN) console.log(`${ts()} [hook] Dry-run mode (no cloud reporting)`);
|
|
431
|
+
|
|
432
|
+
process.on('SIGINT', () => {
|
|
433
|
+
console.log(`\n${ts()} [hook] Shutting down...`);
|
|
434
|
+
const s = tailer.getStats();
|
|
435
|
+
console.log(`${ts()} [hook] Stats: processed=${s.processed} threats=${s.threats} errors=${s.errors}`);
|
|
436
|
+
tailer.stop();
|
|
437
|
+
deliveryWatcher.stop();
|
|
438
|
+
auditStream.end();
|
|
439
|
+
process.exit(0);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// Periodic stats
|
|
443
|
+
setInterval(() => {
|
|
444
|
+
const s = tailer.getStats();
|
|
445
|
+
if (s.processed > 0 || VERBOSE) {
|
|
446
|
+
console.log(`${ts()} [hook] Stats: processed=${s.processed} threats=${s.threats} errors=${s.errors}`);
|
|
447
|
+
}
|
|
448
|
+
}, 30 * 60 * 1000);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ─── Exports (for use by index.js) ───────────────────────────────────────────
|
|
452
|
+
|
|
453
|
+
module.exports = { SessionTailer, DeliveryWatcher, extractContent, loadScanner };
|