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
package/agent/README.md
ADDED
|
@@ -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"
|