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.
Files changed (171) hide show
  1. package/.dockerignore +9 -0
  2. package/CHANGELOG.md +18 -0
  3. package/DEMO.md +87 -0
  4. package/Dockerfile +5 -18
  5. package/README.md +232 -8
  6. package/THREAT_MODEL.md +129 -0
  7. package/agent/README.md +131 -0
  8. package/agent/index.js +471 -0
  9. package/agent/install-service.sh +94 -0
  10. package/agent/openclaw-hook.js +453 -0
  11. package/agent/provider-setup.js +649 -0
  12. package/agent/setup.js +274 -0
  13. package/assets/BADGE-USAGE.md +20 -0
  14. package/assets/clawmoat-badge.svg +21 -0
  15. package/bin/clawmoat.js +468 -111
  16. package/docs/affiliates/dashboard.html +124 -0
  17. package/docs/affiliates/index.html +236 -0
  18. package/docs/agent-install.html +183 -0
  19. package/docs/ai-agent-security-scanner.html +10 -6
  20. package/docs/badge/index.html +149 -0
  21. package/docs/badge/scanning.svg +23 -0
  22. package/docs/blog/386-malicious-skills.html +11 -4
  23. package/docs/blog/40000-exposed-openclaw-instances.html +11 -4
  24. package/docs/blog/agent-trust-protocol.html +5 -4
  25. package/docs/blog/ai-agent-earns-commissions.html +230 -0
  26. package/docs/blog/bugmageddon-agent-firewall.html +174 -0
  27. package/docs/blog/calculator-math.html +180 -0
  28. package/docs/blog/clawmoat-vs-llamafirewall-nemo-guardrails.html +10 -4
  29. package/docs/blog/host-guardian-launch.html +18 -8
  30. package/docs/blog/ibm-experts-agent-runtime-protection.html +15 -6
  31. package/docs/blog/index.html +67 -9
  32. package/docs/blog/langchain-security-tutorial.html +18 -8
  33. package/docs/blog/mcp-30-cves-security-crisis.html +11 -4
  34. package/docs/blog/meta-researcher-rogue-agent.html +201 -0
  35. package/docs/blog/microsoft-openclaw-workstation-security.html +5 -4
  36. package/docs/blog/nist-ai-agent-standards-clawmoat.html +16 -8
  37. package/docs/blog/oasis-websocket-hijack.html +11 -4
  38. package/docs/blog/ollama-openclaw-security.html +10 -4
  39. package/docs/blog/openclaw-enterprise-readiness-claw10.html +5 -4
  40. package/docs/blog/openclaw-security-reckoning-2026.html +11 -4
  41. package/docs/blog/owasp-agentic-ai-top10.html +18 -8
  42. package/docs/blog/securing-ai-agents.html +18 -8
  43. package/docs/blog/supply-chain-agents.html +18 -8
  44. package/docs/business/index.html +11 -16
  45. package/docs/business/install.html +21 -7
  46. package/docs/checklist.html +10 -4
  47. package/docs/compare/index.html +122 -0
  48. package/docs/compare/lakera/index.html +62 -0
  49. package/docs/compare/llm-guard/index.html +49 -0
  50. package/docs/compare/snyk-agent-scan/index.html +63 -0
  51. package/docs/compare.html +10 -6
  52. package/docs/dashboard/index.html +520 -0
  53. package/docs/finance/index.html +9 -6
  54. package/docs/guides/business-deployment.html +770 -0
  55. package/docs/hall-of-fame.html +11 -5
  56. package/docs/index.html +266 -137
  57. package/docs/integrations/langchain.html +14 -6
  58. package/docs/integrations/openai.html +14 -6
  59. package/docs/integrations/openclaw.html +55 -7
  60. package/docs/plans/2026-03-26-threat-intel-api.md +255 -0
  61. package/docs/plans/2026-04-14-bugmageddon-marketing-pack.md +329 -0
  62. package/docs/plans/2026-04-14-clawmoat-v1-bugmageddon.md +248 -0
  63. package/docs/plans/2026-04-14-v1-release-update.md +91 -0
  64. package/docs/plans/2026-04-19-supabase-audit.md +68 -0
  65. package/docs/plans/2026-05-12-sales-push.md +303 -0
  66. package/docs/playground/index.html +893 -0
  67. package/docs/playground.html +4 -7
  68. package/docs/rfcs/defense-in-depth.md +467 -0
  69. package/docs/scan/index.html +156 -12
  70. package/docs/services/case-study.html +255 -0
  71. package/docs/services/downloads/install-openclaw.bat +45 -0
  72. package/docs/services/downloads/install-openclaw.command +38 -0
  73. package/docs/services/downloads/install-openclaw.sh +38 -0
  74. package/docs/services/get-started.html +165 -0
  75. package/docs/services/index.html +598 -0
  76. package/docs/services/multi-agent-security.html +284 -0
  77. package/docs/services/one-pager.html +99 -0
  78. package/docs/services/pitch-deck.html +229 -0
  79. package/docs/services/roi-calculator.html +258 -0
  80. package/docs/sitemap.xml +62 -2
  81. package/docs/support/index.html +12 -1
  82. package/docs/templates/customer-service/HEARTBEAT.md +61 -0
  83. package/docs/templates/customer-service/MEMORY.md +89 -0
  84. package/docs/templates/customer-service/SOUL.md +41 -0
  85. package/docs/templates/customer-service/USER.md +56 -0
  86. package/docs/templates/executive/HEARTBEAT.md +86 -0
  87. package/docs/templates/executive/MEMORY.md +92 -0
  88. package/docs/templates/executive/SOUL.md +44 -0
  89. package/docs/templates/executive/USER.md +62 -0
  90. package/docs/templates/finance/HEARTBEAT.md +58 -0
  91. package/docs/templates/finance/MEMORY.md +87 -0
  92. package/docs/templates/finance/SOUL.md +38 -0
  93. package/docs/templates/finance/USER.md +53 -0
  94. package/docs/templates/index.html +115 -0
  95. package/docs/templates/operations/HEARTBEAT.md +63 -0
  96. package/docs/templates/operations/MEMORY.md +68 -0
  97. package/docs/templates/operations/SOUL.md +38 -0
  98. package/docs/templates/operations/USER.md +49 -0
  99. package/docs/templates/sales/HEARTBEAT.md +55 -0
  100. package/docs/templates/sales/MEMORY.md +89 -0
  101. package/docs/templates/sales/SOUL.md +34 -0
  102. package/docs/templates/sales/USER.md +54 -0
  103. package/eslint.config.js +32 -0
  104. package/evals/README.md +29 -0
  105. package/evals/cases.json +390 -0
  106. package/evals/results.md +68 -0
  107. package/evals/run.js +180 -0
  108. package/examples/demo-attack/demo.js +186 -0
  109. package/examples/python-quickstart/README.md +54 -0
  110. package/examples/python-quickstart/clawmoat_client.py +167 -0
  111. package/examples/video-demo/README.md +14 -0
  112. package/examples/video-demo/scene-a-normal.js +29 -0
  113. package/examples/video-demo/scene-b-attack-arrives.js +31 -0
  114. package/examples/video-demo/scene-c-hijack.js +44 -0
  115. package/examples/video-demo/scene-d-clawmoat.js +46 -0
  116. package/integrations/crewai/README.md +32 -0
  117. package/integrations/crewai/clawmoat_crewai/__init__.py +17 -0
  118. package/integrations/crewai/clawmoat_crewai/guard.py +103 -0
  119. package/integrations/crewai/pyproject.toml +21 -0
  120. package/integrations/langchain/README.md +91 -0
  121. package/integrations/langchain/clawmoat_langchain/__init__.py +17 -0
  122. package/integrations/langchain/clawmoat_langchain/callback.py +489 -0
  123. package/integrations/langchain/pyproject.toml +32 -0
  124. package/integrations/litellm/README.md +324 -0
  125. package/integrations/litellm/clawmoat_litellm/__init__.py +21 -0
  126. package/integrations/litellm/clawmoat_litellm/callback.py +329 -0
  127. package/integrations/litellm/clawmoat_litellm/proxy_middleware.py +224 -0
  128. package/integrations/litellm/pyproject.toml +74 -0
  129. package/integrations/openai-agents/README.md +392 -0
  130. package/integrations/openai-agents/clawmoat_openai_agents/__init__.py +20 -0
  131. package/integrations/openai-agents/clawmoat_openai_agents/guardrail.py +431 -0
  132. package/integrations/openai-agents/clawmoat_openai_agents/middleware.py +311 -0
  133. package/integrations/openai-agents/pyproject.toml +76 -0
  134. package/package.json +6 -5
  135. package/plugins/openclaw-adapter/PHASE1.md +439 -0
  136. package/plugins/openclaw-adapter/README.md +103 -0
  137. package/plugins/openclaw-adapter/SPEC.md +1644 -0
  138. package/plugins/openclaw-adapter/package.json +31 -0
  139. package/plugins/openclaw-adapter/src/index.test.ts +226 -0
  140. package/plugins/openclaw-adapter/src/index.ts +140 -0
  141. package/plugins/openclaw-adapter/tsconfig.json +14 -0
  142. package/server/data/threats.json +290 -0
  143. package/server/index.js +142 -7
  144. package/src/adapters/express.js +161 -0
  145. package/src/adapters/index.js +92 -0
  146. package/src/adapters/langchain.js +185 -0
  147. package/src/approval/index.js +456 -0
  148. package/src/ban-scanner.js +200 -0
  149. package/src/boundary-scanner.js +296 -0
  150. package/src/ci-scanner.js +279 -0
  151. package/src/code-scanner.js +245 -0
  152. package/src/enforce.js +166 -0
  153. package/src/formatters/json.js +80 -0
  154. package/src/formatters/sarif.js +388 -0
  155. package/src/guardian/alerts.js +34 -3
  156. package/src/guardian/index.js +41 -2
  157. package/src/index.js +102 -0
  158. package/src/integrations/agentmesh.js +501 -0
  159. package/src/language-detector.js +201 -0
  160. package/src/mcp-scanner.js +253 -0
  161. package/src/multimodal/index.js +579 -0
  162. package/src/obfuscation-scanner.js +457 -0
  163. package/src/policy-engine.js +402 -0
  164. package/src/scanners/dependency-attacks.js +128 -0
  165. package/src/scanners/prompt-injection.js +18 -0
  166. package/src/scanners/supply-chain.js +14 -0
  167. package/src/templates/default-config.yml +90 -0
  168. package/src/vuln-ops/exploitability.js +46 -0
  169. package/src/watch/live-monitor.js +720 -0
  170. package/clawmoat-0.8.0.tgz +0 -0
  171. package/server/index.js.patch +0 -1
@@ -0,0 +1,131 @@
1
+ # ClawMoat Local Agent
2
+
3
+ A Node.js daemon that monitors OpenClaw activity, scans messages through ClawMoat, and reports results to the cloud dashboard at [app.clawmoat.com](https://app.clawmoat.com).
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # 1. Configure (interactive setup)
9
+ node agent/setup.js
10
+
11
+ # 2. Run the daemon
12
+ node agent/index.js
13
+
14
+ # 3. Or run with verbose output
15
+ node agent/index.js --verbose
16
+ ```
17
+
18
+ ## What it monitors
19
+
20
+ - **`~/.openclaw/agents/main/sessions/*.jsonl`** — Real-time OpenClaw session files. Every inbound (user) and outbound (assistant) message is scanned as it's written.
21
+ - **`~/.openclaw/delivery-queue/`** — Incoming channel messages (Telegram, Discord, etc.) before they reach the agent.
22
+
23
+ ## Files
24
+
25
+ | File | Purpose |
26
+ |------|---------|
27
+ | `index.js` | Main daemon — run this |
28
+ | `setup.js` | Interactive configuration wizard |
29
+ | `install-service.sh` | Install as systemd user service |
30
+ | `openclaw-hook.js` | OpenClaw integration layer (can also run standalone) |
31
+ | `~/.clawmoat/agent.json` | Config (API key, settings) |
32
+ | `~/.clawmoat/audit.log` | Local JSONL audit log of all scans |
33
+
34
+ ## Config (`~/.clawmoat/agent.json`)
35
+
36
+ ```json
37
+ {
38
+ "apiKey": "cm_live_...",
39
+ "dashboardUrl": "https://app.clawmoat.com",
40
+ "scanInbound": true,
41
+ "scanOutbound": true,
42
+ "scanToolCalls": true,
43
+ "auditLog": "~/.clawmoat/audit.log",
44
+ "reportToCloud": true
45
+ }
46
+ ```
47
+
48
+ Get your API key from: https://app.clawmoat.com/settings/api-keys
49
+
50
+ ## Systemd Service (WSL2)
51
+
52
+ First enable systemd in WSL2 (`/etc/wsl.conf`):
53
+ ```ini
54
+ [boot]
55
+ systemd=true
56
+ ```
57
+
58
+ Then run setup:
59
+ ```bash
60
+ node agent/setup.js
61
+ # Answer yes to "Install as systemd user service?"
62
+ ```
63
+
64
+ Or manually:
65
+ ```bash
66
+ bash agent/install-service.sh
67
+ systemctl --user status clawmoat-agent
68
+ journalctl --user -u clawmoat-agent -f
69
+ ```
70
+
71
+ ## Cloud API
72
+
73
+ Each scan posts to `POST /api/scan` with Bearer auth:
74
+
75
+ ```json
76
+ {
77
+ "source": "local-agent",
78
+ "agentVersion": "1.0.0",
79
+ "hostname": "DarLaptop",
80
+ "meta": {
81
+ "direction": "inbound",
82
+ "role": "user",
83
+ "sessionFile": "abc123",
84
+ "timestamp": "2026-03-12T..."
85
+ },
86
+ "result": {
87
+ "safe": false,
88
+ "severity": "high",
89
+ "action": "block",
90
+ "findings": [...]
91
+ }
92
+ }
93
+ ```
94
+
95
+ Cloud reporting is skipped silently if `apiKey` is not set or is the placeholder value.
96
+
97
+ ## Dry Run / Testing
98
+
99
+ ```bash
100
+ # No cloud calls, verbose output
101
+ node agent/index.js --dry-run --verbose
102
+
103
+ # Hook standalone (same flags)
104
+ node agent/openclaw-hook.js --verbose
105
+ ```
106
+
107
+ ## Architecture
108
+
109
+ ```
110
+ OpenClaw session files (.jsonl)
111
+
112
+
113
+ SessionTailer (fs.watch)
114
+ │ new lines
115
+
116
+ extractContent()
117
+ │ text + role
118
+
119
+ ClawMoat.scanInbound/scanOutbound()
120
+
121
+ ┌────┴────┐
122
+ │ │
123
+ CLEAN THREAT
124
+ │ │
125
+ audit audit + cloud POST
126
+ log │
127
+ reportToCloud()
128
+
129
+ app.clawmoat.com
130
+ /api/scan
131
+ ```
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"