clawmoat 0.7.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.
Files changed (178) hide show
  1. package/.dockerignore +9 -0
  2. package/CHANGELOG.md +18 -0
  3. package/CONTRIBUTING.md +4 -2
  4. package/DEMO.md +87 -0
  5. package/Dockerfile +5 -18
  6. package/README.md +294 -8
  7. package/SECURITY.md +58 -10
  8. package/THREAT_MODEL.md +129 -0
  9. package/agent/README.md +131 -0
  10. package/agent/index.js +471 -0
  11. package/agent/install-service.sh +94 -0
  12. package/agent/openclaw-hook.js +453 -0
  13. package/agent/provider-setup.js +649 -0
  14. package/agent/setup.js +274 -0
  15. package/assets/BADGE-USAGE.md +20 -0
  16. package/assets/clawmoat-badge.svg +21 -0
  17. package/bin/clawmoat.js +468 -111
  18. package/docs/affiliates/dashboard.html +124 -0
  19. package/docs/affiliates/index.html +236 -0
  20. package/docs/agent-install.html +183 -0
  21. package/docs/ai-agent-security-scanner.html +10 -6
  22. package/docs/badge/index.html +149 -0
  23. package/docs/badge/scanning.svg +23 -0
  24. package/docs/blog/386-malicious-skills.html +262 -0
  25. package/docs/blog/40000-exposed-openclaw-instances.html +201 -0
  26. package/docs/blog/agent-trust-protocol.html +198 -0
  27. package/docs/blog/ai-agent-earns-commissions.html +230 -0
  28. package/docs/blog/bugmageddon-agent-firewall.html +174 -0
  29. package/docs/blog/calculator-math.html +180 -0
  30. package/docs/blog/clawmoat-vs-llamafirewall-nemo-guardrails.html +229 -0
  31. package/docs/blog/host-guardian-launch.html +18 -8
  32. package/docs/blog/ibm-experts-agent-runtime-protection.html +247 -0
  33. package/docs/blog/index.html +211 -9
  34. package/docs/blog/langchain-security-tutorial.html +18 -8
  35. package/docs/blog/mcp-30-cves-security-crisis.html +286 -0
  36. package/docs/blog/meta-researcher-rogue-agent.html +201 -0
  37. package/docs/blog/microsoft-openclaw-workstation-security.html +235 -0
  38. package/docs/blog/nist-ai-agent-standards-clawmoat.html +377 -0
  39. package/docs/blog/oasis-websocket-hijack.html +212 -0
  40. package/docs/blog/ollama-openclaw-security.html +160 -0
  41. package/docs/blog/openclaw-enterprise-readiness-claw10.html +199 -0
  42. package/docs/blog/openclaw-security-reckoning-2026.html +368 -0
  43. package/docs/blog/owasp-agentic-ai-top10.html +18 -8
  44. package/docs/blog/securing-ai-agents.html +18 -8
  45. package/docs/blog/supply-chain-agents.html +18 -8
  46. package/docs/business/index.html +525 -0
  47. package/docs/business/install.html +261 -0
  48. package/docs/checklist.html +174 -0
  49. package/docs/compare/index.html +122 -0
  50. package/docs/compare/lakera/index.html +62 -0
  51. package/docs/compare/llm-guard/index.html +49 -0
  52. package/docs/compare/snyk-agent-scan/index.html +63 -0
  53. package/docs/compare.html +10 -6
  54. package/docs/dashboard/index.html +520 -0
  55. package/docs/finance/index.html +220 -0
  56. package/docs/guides/business-deployment.html +770 -0
  57. package/docs/hall-of-fame.html +174 -0
  58. package/docs/index.html +447 -154
  59. package/docs/install.sh +557 -0
  60. package/docs/integrations/langchain.html +14 -6
  61. package/docs/integrations/openai.html +14 -6
  62. package/docs/integrations/openclaw.html +55 -7
  63. package/docs/plans/2026-03-26-threat-intel-api.md +255 -0
  64. package/docs/plans/2026-04-14-bugmageddon-marketing-pack.md +329 -0
  65. package/docs/plans/2026-04-14-clawmoat-v1-bugmageddon.md +248 -0
  66. package/docs/plans/2026-04-14-v1-release-update.md +91 -0
  67. package/docs/plans/2026-04-19-supabase-audit.md +68 -0
  68. package/docs/plans/2026-05-12-sales-push.md +303 -0
  69. package/docs/playground/index.html +893 -0
  70. package/docs/playground.html +4 -7
  71. package/docs/privacy-policy/index.html +122 -0
  72. package/docs/rfcs/defense-in-depth.md +467 -0
  73. package/docs/scan/index.html +358 -0
  74. package/docs/services/case-study.html +255 -0
  75. package/docs/services/downloads/install-openclaw.bat +45 -0
  76. package/docs/services/downloads/install-openclaw.command +38 -0
  77. package/docs/services/downloads/install-openclaw.sh +38 -0
  78. package/docs/services/get-started.html +165 -0
  79. package/docs/services/index.html +598 -0
  80. package/docs/services/multi-agent-security.html +284 -0
  81. package/docs/services/one-pager.html +99 -0
  82. package/docs/services/pitch-deck.html +229 -0
  83. package/docs/services/roi-calculator.html +258 -0
  84. package/docs/sitemap.xml +192 -2
  85. package/docs/support/index.html +135 -0
  86. package/docs/templates/customer-service/HEARTBEAT.md +61 -0
  87. package/docs/templates/customer-service/MEMORY.md +89 -0
  88. package/docs/templates/customer-service/SOUL.md +41 -0
  89. package/docs/templates/customer-service/USER.md +56 -0
  90. package/docs/templates/executive/HEARTBEAT.md +86 -0
  91. package/docs/templates/executive/MEMORY.md +92 -0
  92. package/docs/templates/executive/SOUL.md +44 -0
  93. package/docs/templates/executive/USER.md +62 -0
  94. package/docs/templates/finance/HEARTBEAT.md +58 -0
  95. package/docs/templates/finance/MEMORY.md +87 -0
  96. package/docs/templates/finance/SOUL.md +38 -0
  97. package/docs/templates/finance/USER.md +53 -0
  98. package/docs/templates/index.html +115 -0
  99. package/docs/templates/operations/HEARTBEAT.md +63 -0
  100. package/docs/templates/operations/MEMORY.md +68 -0
  101. package/docs/templates/operations/SOUL.md +38 -0
  102. package/docs/templates/operations/USER.md +49 -0
  103. package/docs/templates/sales/HEARTBEAT.md +55 -0
  104. package/docs/templates/sales/MEMORY.md +89 -0
  105. package/docs/templates/sales/SOUL.md +34 -0
  106. package/docs/templates/sales/USER.md +54 -0
  107. package/docs/terms-of-service/index.html +122 -0
  108. package/eslint.config.js +32 -0
  109. package/evals/README.md +29 -0
  110. package/evals/cases.json +390 -0
  111. package/evals/results.md +68 -0
  112. package/evals/run.js +180 -0
  113. package/examples/basic-usage.js +38 -0
  114. package/examples/demo-attack/demo.js +186 -0
  115. package/examples/python-quickstart/README.md +54 -0
  116. package/examples/python-quickstart/clawmoat_client.py +167 -0
  117. package/examples/video-demo/README.md +14 -0
  118. package/examples/video-demo/scene-a-normal.js +29 -0
  119. package/examples/video-demo/scene-b-attack-arrives.js +31 -0
  120. package/examples/video-demo/scene-c-hijack.js +44 -0
  121. package/examples/video-demo/scene-d-clawmoat.js +46 -0
  122. package/integrations/crewai/README.md +32 -0
  123. package/integrations/crewai/clawmoat_crewai/__init__.py +17 -0
  124. package/integrations/crewai/clawmoat_crewai/guard.py +103 -0
  125. package/integrations/crewai/pyproject.toml +21 -0
  126. package/integrations/langchain/README.md +91 -0
  127. package/integrations/langchain/clawmoat_langchain/__init__.py +17 -0
  128. package/integrations/langchain/clawmoat_langchain/callback.py +489 -0
  129. package/integrations/langchain/pyproject.toml +32 -0
  130. package/integrations/litellm/README.md +324 -0
  131. package/integrations/litellm/clawmoat_litellm/__init__.py +21 -0
  132. package/integrations/litellm/clawmoat_litellm/callback.py +329 -0
  133. package/integrations/litellm/clawmoat_litellm/proxy_middleware.py +224 -0
  134. package/integrations/litellm/pyproject.toml +74 -0
  135. package/integrations/openai-agents/README.md +392 -0
  136. package/integrations/openai-agents/clawmoat_openai_agents/__init__.py +20 -0
  137. package/integrations/openai-agents/clawmoat_openai_agents/guardrail.py +431 -0
  138. package/integrations/openai-agents/clawmoat_openai_agents/middleware.py +311 -0
  139. package/integrations/openai-agents/pyproject.toml +76 -0
  140. package/package.json +6 -5
  141. package/plugins/openclaw-adapter/PHASE1.md +439 -0
  142. package/plugins/openclaw-adapter/README.md +103 -0
  143. package/plugins/openclaw-adapter/SPEC.md +1644 -0
  144. package/plugins/openclaw-adapter/package.json +31 -0
  145. package/plugins/openclaw-adapter/src/index.test.ts +226 -0
  146. package/plugins/openclaw-adapter/src/index.ts +140 -0
  147. package/plugins/openclaw-adapter/tsconfig.json +14 -0
  148. package/server/data/threats.json +290 -0
  149. package/server/index.js +224 -10
  150. package/src/adapters/express.js +161 -0
  151. package/src/adapters/index.js +92 -0
  152. package/src/adapters/langchain.js +185 -0
  153. package/src/approval/index.js +456 -0
  154. package/src/ban-scanner.js +200 -0
  155. package/src/boundary-scanner.js +296 -0
  156. package/src/ci-scanner.js +279 -0
  157. package/src/code-scanner.js +245 -0
  158. package/src/enforce.js +166 -0
  159. package/src/finance/index.js +585 -0
  160. package/src/finance/mcp-firewall.js +486 -0
  161. package/src/formatters/json.js +80 -0
  162. package/src/formatters/sarif.js +388 -0
  163. package/src/guardian/alerts.js +34 -3
  164. package/src/guardian/gateway-monitor.js +590 -0
  165. package/src/guardian/index.js +41 -2
  166. package/src/index.js +105 -0
  167. package/src/integrations/agentmesh.js +501 -0
  168. package/src/language-detector.js +201 -0
  169. package/src/mcp-scanner.js +253 -0
  170. package/src/multimodal/index.js +579 -0
  171. package/src/obfuscation-scanner.js +457 -0
  172. package/src/policy-engine.js +402 -0
  173. package/src/scanners/dependency-attacks.js +128 -0
  174. package/src/scanners/prompt-injection.js +18 -0
  175. package/src/scanners/supply-chain.js +14 -0
  176. package/src/templates/default-config.yml +90 -0
  177. package/src/vuln-ops/exploitability.js +46 -0
  178. package/src/watch/live-monitor.js +720 -0
package/agent/index.js ADDED
@@ -0,0 +1,471 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ClawMoat Local Agent — Main Daemon
4
+ *
5
+ * Monitors OpenClaw session activity, scans messages through ClawMoat,
6
+ * reports results to the cloud dashboard, and maintains a local audit log.
7
+ *
8
+ * Usage: node agent/index.js [--config <path>] [--dry-run] [--verbose]
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const os = require('os');
16
+ const https = require('https');
17
+ const http = require('http');
18
+ const readline = require('readline');
19
+ const { EventEmitter } = require('events');
20
+
21
+ // ─── Config ───────────────────────────────────────────────────────────────────
22
+
23
+ const DEFAULT_CONFIG_PATH = path.join(os.homedir(), '.clawmoat', 'agent.json');
24
+ const OPENCLAW_SESSIONS_DIR = path.join(os.homedir(), '.openclaw', 'agents', 'main', 'sessions');
25
+
26
+ const args = process.argv.slice(2);
27
+ const DRY_RUN = args.includes('--dry-run');
28
+ const VERBOSE = args.includes('--verbose') || args.includes('-v');
29
+ const configPath = (() => {
30
+ const i = args.indexOf('--config');
31
+ return i >= 0 ? args[i + 1] : DEFAULT_CONFIG_PATH;
32
+ })();
33
+
34
+ function loadConfig() {
35
+ try {
36
+ const raw = fs.readFileSync(configPath, 'utf8');
37
+ return JSON.parse(raw);
38
+ } catch (e) {
39
+ if (e.code === 'ENOENT') {
40
+ console.error(`[clawmoat-agent] Config not found at ${configPath}`);
41
+ console.error(`[clawmoat-agent] Run: node agent/setup.js`);
42
+ process.exit(1);
43
+ }
44
+ console.error(`[clawmoat-agent] Failed to parse config: ${e.message}`);
45
+ process.exit(1);
46
+ }
47
+ }
48
+
49
+ let config = loadConfig();
50
+
51
+ function resolveAuditLog() {
52
+ const raw = config.auditLog || '~/.clawmoat/audit.log';
53
+ return raw.replace(/^~/, os.homedir());
54
+ }
55
+
56
+ // ─── ClawMoat Scanner ─────────────────────────────────────────────────────────
57
+
58
+ let moat;
59
+ try {
60
+ const ClawMoat = require(path.join(os.homedir(), 'clawmoat', 'src', 'index.js'));
61
+ moat = new ClawMoat({ quiet: true });
62
+ log('ClawMoat scanner loaded from ~/clawmoat');
63
+ } catch (e) {
64
+ try {
65
+ const { ClawMoat } = require('clawmoat');
66
+ moat = new ClawMoat({ quiet: true });
67
+ log('ClawMoat scanner loaded from npm');
68
+ } catch (e2) {
69
+ console.error('[clawmoat-agent] Cannot load ClawMoat:', e.message);
70
+ process.exit(1);
71
+ }
72
+ }
73
+
74
+ // ─── Logging ──────────────────────────────────────────────────────────────────
75
+
76
+ function log(...args) {
77
+ if (VERBOSE || args[0]?.includes('ERROR') || args[0]?.includes('THREAT')) {
78
+ console.log(new Date().toISOString(), '[clawmoat-agent]', ...args);
79
+ }
80
+ }
81
+
82
+ function logAlways(...args) {
83
+ console.log(new Date().toISOString(), '[clawmoat-agent]', ...args);
84
+ }
85
+
86
+ // ─── Audit Log ────────────────────────────────────────────────────────────────
87
+
88
+ let auditStream;
89
+
90
+ function initAuditLog() {
91
+ const auditPath = resolveAuditLog();
92
+ fs.mkdirSync(path.dirname(auditPath), { recursive: true });
93
+ auditStream = fs.createWriteStream(auditPath, { flags: 'a' });
94
+ log(`Audit log: ${auditPath}`);
95
+ }
96
+
97
+ function writeAudit(entry) {
98
+ const line = JSON.stringify({ ...entry, agentVersion: '1.0.0', ts: new Date().toISOString() });
99
+ if (auditStream) auditStream.write(line + '\n');
100
+ }
101
+
102
+ // ─── Cloud Reporting ──────────────────────────────────────────────────────────
103
+
104
+ const RETRY_DELAYS = [1000, 5000, 15000, 60000]; // ms
105
+
106
+ async function reportToCloud(scanResult, meta) {
107
+ if (!config.reportToCloud || DRY_RUN) {
108
+ if (DRY_RUN) log('[DRY-RUN] Would report to cloud:', JSON.stringify(scanResult).slice(0, 100));
109
+ return;
110
+ }
111
+ if (!config.apiKey || config.apiKey === 'cm_live_...') {
112
+ log('No valid API key — skipping cloud report');
113
+ return;
114
+ }
115
+
116
+ const dashboardUrl = config.dashboardUrl || 'https://app.clawmoat.com';
117
+ const textPreview = meta?.text ? meta.text.slice(0, 500) : '[agent scan]';
118
+ const payload = JSON.stringify({
119
+ text: textPreview,
120
+ source: 'local-agent',
121
+ agentVersion: '1.0.0',
122
+ hostname: os.hostname(),
123
+ meta,
124
+ results: scanResult,
125
+ });
126
+
127
+ for (let attempt = 0; attempt <= RETRY_DELAYS.length; attempt++) {
128
+ try {
129
+ await httpPost(`${dashboardUrl}/api/scan`, payload, {
130
+ 'Authorization': `Bearer ${config.apiKey}`,
131
+ 'Content-Type': 'application/json',
132
+ 'Content-Length': Buffer.byteLength(payload),
133
+ });
134
+ log('Reported to cloud dashboard');
135
+ return;
136
+ } catch (e) {
137
+ if (attempt < RETRY_DELAYS.length) {
138
+ const delay = RETRY_DELAYS[attempt];
139
+ log(`Cloud report failed (attempt ${attempt + 1}): ${e.message} — retrying in ${delay}ms`);
140
+ await sleep(delay);
141
+ } else {
142
+ log(`ERROR: Cloud report failed after all retries: ${e.message}`);
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ function httpPost(url, body, headers) {
149
+ return new Promise((resolve, reject) => {
150
+ const parsed = new URL(url);
151
+ const lib = parsed.protocol === 'https:' ? https : http;
152
+ const req = lib.request({
153
+ hostname: parsed.hostname,
154
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
155
+ path: parsed.pathname + parsed.search,
156
+ method: 'POST',
157
+ headers,
158
+ timeout: 10000,
159
+ }, res => {
160
+ let data = '';
161
+ res.on('data', chunk => data += chunk);
162
+ res.on('end', () => {
163
+ if (res.statusCode >= 200 && res.statusCode < 300) {
164
+ resolve(data);
165
+ } else {
166
+ reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`));
167
+ }
168
+ });
169
+ });
170
+ req.on('error', reject);
171
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
172
+ req.write(body);
173
+ req.end();
174
+ });
175
+ }
176
+
177
+ function sleep(ms) {
178
+ return new Promise(r => setTimeout(r, ms));
179
+ }
180
+
181
+ // ─── Scan Engine ──────────────────────────────────────────────────────────────
182
+
183
+ async function scanAndReport(text, meta) {
184
+ if (!text || typeof text !== 'string' || text.trim().length === 0) return;
185
+
186
+ let result;
187
+ try {
188
+ if (meta.direction === 'inbound' && config.scanInbound !== false) {
189
+ result = moat.scanInbound(text);
190
+ } else if (meta.direction === 'outbound' && config.scanOutbound !== false) {
191
+ result = moat.scanOutbound(text);
192
+ } else {
193
+ return;
194
+ }
195
+ } catch (e) {
196
+ log(`ERROR: Scan failed: ${e.message}`);
197
+ return;
198
+ }
199
+
200
+ const entry = { meta, result, text: text.slice(0, 500) };
201
+ writeAudit(entry);
202
+
203
+ if (!result.safe || VERBOSE) {
204
+ const label = result.safe ? 'CLEAN' : `THREAT[${result.severity}]`;
205
+ const findings = result.findings?.length
206
+ ? ` — ${result.findings.map(f => `${f.type}:${f.subtype}`).join(', ')}`
207
+ : '';
208
+ logAlways(`${label} ${meta.direction} [${meta.role}] ${findings} | "${text.slice(0, 80).replace(/\n/g, ' ')}"`);
209
+ }
210
+
211
+ if (!result.safe) {
212
+ await reportToCloud(result, { ...meta, text });
213
+ }
214
+ }
215
+
216
+ // ─── Session File Watcher ─────────────────────────────────────────────────────
217
+
218
+ class SessionWatcher extends EventEmitter {
219
+ constructor(sessionsDir) {
220
+ super();
221
+ this.sessionsDir = sessionsDir;
222
+ this.watched = new Map(); // file → { fd, position, watcher }
223
+ this.dirWatcher = null;
224
+ }
225
+
226
+ start() {
227
+ if (!fs.existsSync(this.sessionsDir)) {
228
+ log(`WARNING: Sessions dir not found: ${this.sessionsDir} — watching parent`);
229
+ // Watch parent for when sessions dir appears
230
+ const parent = path.dirname(this.sessionsDir);
231
+ if (fs.existsSync(parent)) {
232
+ fs.watch(parent, (event, filename) => {
233
+ if (filename === 'sessions' && fs.existsSync(this.sessionsDir)) {
234
+ this._watchDir();
235
+ }
236
+ });
237
+ }
238
+ return;
239
+ }
240
+ this._watchDir();
241
+ }
242
+
243
+ _watchDir() {
244
+ log(`Watching sessions: ${this.sessionsDir}`);
245
+
246
+ // Watch existing files
247
+ try {
248
+ const files = fs.readdirSync(this.sessionsDir).filter(f => f.endsWith('.jsonl'));
249
+ for (const file of files) {
250
+ this._startWatchingFile(path.join(this.sessionsDir, file));
251
+ }
252
+ } catch (e) {
253
+ log(`ERROR reading sessions dir: ${e.message}`);
254
+ }
255
+
256
+ // Watch for new files
257
+ this.dirWatcher = fs.watch(this.sessionsDir, (event, filename) => {
258
+ if (!filename?.endsWith('.jsonl')) return;
259
+ const fullPath = path.join(this.sessionsDir, filename);
260
+ if (!this.watched.has(fullPath) && fs.existsSync(fullPath)) {
261
+ log(`New session file: ${filename}`);
262
+ this._startWatchingFile(fullPath);
263
+ }
264
+ });
265
+ }
266
+
267
+ _startWatchingFile(filePath) {
268
+ if (this.watched.has(filePath)) return;
269
+
270
+ let position;
271
+ try {
272
+ const stat = fs.statSync(filePath);
273
+ // For existing files, start at end (don't re-scan history)
274
+ position = stat.size;
275
+ } catch (e) {
276
+ position = 0;
277
+ }
278
+
279
+ const state = { position, pending: '' };
280
+ this.watched.set(filePath, state);
281
+
282
+ const watcher = fs.watch(filePath, (event) => {
283
+ if (event === 'change') {
284
+ this._readNewLines(filePath, state);
285
+ }
286
+ });
287
+
288
+ watcher.on('error', () => {
289
+ this.watched.delete(filePath);
290
+ });
291
+
292
+ state.watcher = watcher;
293
+ log(`Watching file: ${path.basename(filePath)} (from byte ${position})`);
294
+ }
295
+
296
+ _readNewLines(filePath, state) {
297
+ let fd;
298
+ try {
299
+ fd = fs.openSync(filePath, 'r');
300
+ const stat = fs.fstatSync(fd);
301
+ if (stat.size <= state.position) return;
302
+
303
+ const chunkSize = Math.min(stat.size - state.position, 65536);
304
+ const buf = Buffer.alloc(chunkSize);
305
+ const bytesRead = fs.readSync(fd, buf, 0, chunkSize, state.position);
306
+ state.position += bytesRead;
307
+
308
+ const text = state.pending + buf.slice(0, bytesRead).toString('utf8');
309
+ const lines = text.split('\n');
310
+ state.pending = lines.pop(); // last partial line
311
+
312
+ for (const line of lines) {
313
+ if (line.trim()) {
314
+ try {
315
+ const entry = JSON.parse(line);
316
+ this.emit('entry', entry, filePath);
317
+ } catch (e) {
318
+ // partial JSON, skip
319
+ }
320
+ }
321
+ }
322
+ } catch (e) {
323
+ log(`ERROR reading ${path.basename(filePath)}: ${e.message}`);
324
+ } finally {
325
+ if (fd !== undefined) {
326
+ try { fs.closeSync(fd); } catch {}
327
+ }
328
+ }
329
+ }
330
+
331
+ stop() {
332
+ if (this.dirWatcher) this.dirWatcher.close();
333
+ for (const [, state] of this.watched) {
334
+ if (state.watcher) state.watcher.close();
335
+ }
336
+ this.watched.clear();
337
+ }
338
+ }
339
+
340
+ // ─── Message Parser ───────────────────────────────────────────────────────────
341
+
342
+ function parseEntry(entry, filePath) {
343
+ // Only process message events
344
+ if (entry.type !== 'message' || !entry.message) return null;
345
+
346
+ const msg = entry.message;
347
+ const role = msg.role; // 'user' | 'assistant'
348
+ const timestamp = entry.timestamp || msg.timestamp;
349
+
350
+ // Extract text content
351
+ const texts = [];
352
+ if (Array.isArray(msg.content)) {
353
+ for (const block of msg.content) {
354
+ if (block.type === 'text' && block.text) {
355
+ texts.push(block.text);
356
+ }
357
+ }
358
+ } else if (typeof msg.content === 'string') {
359
+ texts.push(msg.content);
360
+ }
361
+
362
+ if (texts.length === 0) return null;
363
+
364
+ return {
365
+ text: texts.join('\n'),
366
+ role,
367
+ direction: role === 'user' ? 'inbound' : 'outbound',
368
+ sessionFile: path.basename(filePath, '.jsonl'),
369
+ messageId: entry.id,
370
+ timestamp,
371
+ };
372
+ }
373
+
374
+ // ─── Workspace Watcher (fallback) ─────────────────────────────────────────────
375
+ // If no sessions, also watch workspace for changes as a secondary signal
376
+
377
+ class WorkspaceWatcher extends EventEmitter {
378
+ constructor(workspaceDir) {
379
+ super();
380
+ this.dir = workspaceDir;
381
+ this.watcher = null;
382
+ }
383
+
384
+ start() {
385
+ if (!fs.existsSync(this.dir)) return;
386
+ log(`Watching workspace: ${this.dir}`);
387
+ // Shallow watch — just detect activity, don't scan file contents
388
+ this.watcher = fs.watch(this.dir, { recursive: false }, (event, filename) => {
389
+ if (filename && !filename.startsWith('.')) {
390
+ this.emit('activity', { event, filename });
391
+ }
392
+ });
393
+ }
394
+
395
+ stop() {
396
+ if (this.watcher) this.watcher.close();
397
+ }
398
+ }
399
+
400
+ // ─── Heartbeat / Stats ────────────────────────────────────────────────────────
401
+
402
+ let stats = { scanned: 0, threats: 0, errors: 0, startedAt: new Date().toISOString() };
403
+
404
+ setInterval(() => {
405
+ logAlways(`[heartbeat] scanned=${stats.scanned} threats=${stats.threats} errors=${stats.errors} uptime=${Math.floor((Date.now() - new Date(stats.startedAt)) / 1000)}s`);
406
+ }, 60 * 60 * 1000); // every hour
407
+
408
+ // ─── Main ─────────────────────────────────────────────────────────────────────
409
+
410
+ async function main() {
411
+ logAlways('ClawMoat Local Agent starting...');
412
+ logAlways(`Config: ${configPath}`);
413
+ logAlways(`Dry-run: ${DRY_RUN} | Verbose: ${VERBOSE}`);
414
+ logAlways(`Scan inbound: ${config.scanInbound !== false} | outbound: ${config.scanOutbound !== false}`);
415
+ logAlways(`Cloud reporting: ${config.reportToCloud && config.apiKey && config.apiKey !== 'cm_live_...' ? 'enabled' : 'disabled (no valid API key)'}`);
416
+
417
+ initAuditLog();
418
+
419
+ // Watch OpenClaw sessions
420
+ const watcher = new SessionWatcher(OPENCLAW_SESSIONS_DIR);
421
+
422
+ watcher.on('entry', async (entry, filePath) => {
423
+ const parsed = parseEntry(entry, filePath);
424
+ if (!parsed) return;
425
+
426
+ stats.scanned++;
427
+ try {
428
+ await scanAndReport(parsed.text, {
429
+ direction: parsed.direction,
430
+ role: parsed.role,
431
+ sessionFile: parsed.sessionFile,
432
+ messageId: parsed.messageId,
433
+ timestamp: parsed.timestamp,
434
+ });
435
+ if (moat.stats?.blocked > (stats.threats || 0)) {
436
+ stats.threats = moat.stats.blocked;
437
+ }
438
+ } catch (e) {
439
+ stats.errors++;
440
+ log(`ERROR: ${e.message}`);
441
+ }
442
+ });
443
+
444
+ watcher.start();
445
+
446
+ // Also watch for hooks directory for future hook integration
447
+ const hooksDir = path.join(os.homedir(), '.openclaw', 'hooks');
448
+ if (fs.existsSync(hooksDir)) {
449
+ log(`OpenClaw hooks dir found at ${hooksDir}`);
450
+ }
451
+
452
+ logAlways('Agent running. Press Ctrl+C to stop.');
453
+ logAlways(`Monitoring: ${OPENCLAW_SESSIONS_DIR}`);
454
+
455
+ // Graceful shutdown
456
+ process.on('SIGTERM', shutdown);
457
+ process.on('SIGINT', shutdown);
458
+
459
+ function shutdown() {
460
+ logAlways('Shutting down...');
461
+ watcher.stop();
462
+ if (auditStream) auditStream.end();
463
+ logAlways(`Final stats: scanned=${stats.scanned} threats=${stats.threats} errors=${stats.errors}`);
464
+ process.exit(0);
465
+ }
466
+ }
467
+
468
+ main().catch(e => {
469
+ console.error('[clawmoat-agent] FATAL:', e);
470
+ process.exit(1);
471
+ });
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env bash
2
+ # ClawMoat Agent — Systemd User Service Installer
3
+ #
4
+ # Usage: ./install-service.sh [node-path] [agent-script-path]
5
+ # Or called from setup.js with those args.
6
+
7
+ set -euo pipefail
8
+
9
+ NODE_BIN="${1:-$(which node)}"
10
+ AGENT_SCRIPT="${2:-$(dirname "$0")/index.js}"
11
+ SERVICE_NAME="clawmoat-agent"
12
+ USER_SYSTEMD_DIR="$HOME/.config/systemd/user"
13
+ SERVICE_FILE="$USER_SYSTEMD_DIR/$SERVICE_NAME.service"
14
+
15
+ echo "Installing ClawMoat agent as systemd user service..."
16
+ echo " Node: $NODE_BIN"
17
+ echo " Script: $AGENT_SCRIPT"
18
+
19
+ # Check prerequisites
20
+ if ! command -v systemctl &>/dev/null; then
21
+ echo "ERROR: systemctl not found. Cannot install systemd service."
22
+ echo "To run manually: node $AGENT_SCRIPT"
23
+ exit 1
24
+ fi
25
+
26
+ # Create systemd user dir
27
+ mkdir -p "$USER_SYSTEMD_DIR"
28
+
29
+ # Write service file
30
+ cat > "$SERVICE_FILE" << EOF
31
+ [Unit]
32
+ Description=ClawMoat Local Security Agent
33
+ Documentation=https://app.clawmoat.com
34
+ After=network-online.target
35
+ Wants=network-online.target
36
+
37
+ [Service]
38
+ Type=simple
39
+ ExecStart=$NODE_BIN $AGENT_SCRIPT
40
+ WorkingDirectory=$HOME
41
+ Restart=on-failure
42
+ RestartSec=10
43
+ StandardOutput=journal
44
+ StandardError=journal
45
+ SyslogIdentifier=clawmoat-agent
46
+
47
+ # Environment
48
+ Environment=NODE_ENV=production
49
+ Environment=HOME=$HOME
50
+
51
+ # Resource limits (lightweight)
52
+ CPUQuota=10%
53
+ MemoryMax=128M
54
+
55
+ [Install]
56
+ WantedBy=default.target
57
+ EOF
58
+
59
+ echo "✓ Service file written: $SERVICE_FILE"
60
+
61
+ # Enable and start
62
+ if systemctl --user daemon-reload 2>/dev/null; then
63
+ echo "✓ Systemd daemon reloaded"
64
+
65
+ if systemctl --user enable "$SERVICE_NAME" 2>/dev/null; then
66
+ echo "✓ Service enabled (auto-start on login)"
67
+ else
68
+ echo " Warning: Could not enable service (WSL2 may require --user session)"
69
+ fi
70
+
71
+ if systemctl --user start "$SERVICE_NAME" 2>/dev/null; then
72
+ echo "✓ Service started"
73
+ sleep 1
74
+ systemctl --user status "$SERVICE_NAME" --no-pager 2>/dev/null || true
75
+ else
76
+ echo " Warning: Could not start service now"
77
+ echo " Try manually: systemctl --user start $SERVICE_NAME"
78
+ fi
79
+ else
80
+ echo ""
81
+ echo " Note: systemd --user session not active."
82
+ echo " In WSL2, enable systemd in /etc/wsl.conf:"
83
+ echo " [boot]"
84
+ echo " systemd=true"
85
+ echo ""
86
+ echo " Then restart WSL and run: systemctl --user enable $SERVICE_NAME"
87
+ fi
88
+
89
+ echo ""
90
+ echo "Service management:"
91
+ echo " Status: systemctl --user status $SERVICE_NAME"
92
+ echo " Logs: journalctl --user -u $SERVICE_NAME -f"
93
+ echo " Stop: systemctl --user stop $SERVICE_NAME"
94
+ echo " Disable: systemctl --user disable $SERVICE_NAME"