openclaw-guardian 0.1.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/LICENSE +21 -0
- package/README.md +279 -0
- package/default-policies.json +3 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +96 -0
- package/dist/index.js.map +1 -0
- package/dist/src/audit-log.d.ts +19 -0
- package/dist/src/audit-log.d.ts.map +1 -0
- package/dist/src/audit-log.js +64 -0
- package/dist/src/audit-log.js.map +1 -0
- package/dist/src/blacklist.d.ts +33 -0
- package/dist/src/blacklist.d.ts.map +1 -0
- package/dist/src/blacklist.js +253 -0
- package/dist/src/blacklist.js.map +1 -0
- package/dist/src/llm-voter.d.ts +28 -0
- package/dist/src/llm-voter.d.ts.map +1 -0
- package/dist/src/llm-voter.js +281 -0
- package/dist/src/llm-voter.js.map +1 -0
- package/openclaw.plugin.json +13 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 fatcatMaoFei
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# openclaw-guardian
|
|
2
|
+
|
|
3
|
+
> **The missing safety layer for AI agents.**
|
|
4
|
+
|
|
5
|
+
## Why This Exists
|
|
6
|
+
|
|
7
|
+
OpenClaw is powerful — it gives AI agents direct access to shell commands, file operations, email, browser automation, and more. That power is exactly what makes it useful, but it's also what makes people nervous.
|
|
8
|
+
|
|
9
|
+
The community has been vocal: *"security nightmare"*, *"what if the AI deletes my files?"*, *"I don't trust it with my credentials"*. OpenClaw's existing safety (sandbox + allowlist + manual confirmation) only covers `exec`, and it's all-or-nothing — either you trust the agent completely, or you block everything.
|
|
10
|
+
|
|
11
|
+
**openclaw-guardian** fills that gap. It sits between the AI's decision and the actual execution, using a two-tier blacklist to catch dangerous operations and LLM-based intent verification to confirm the user actually asked for them. Think of it as a security checkpoint that only stops you when you're carrying something dangerous — and even then, it just checks your ID before letting you through.
|
|
12
|
+
|
|
13
|
+
The key insight: **99% of what an AI agent does is harmless** (reading files, fetching URLs, writing notes). Only ~1% is potentially dangerous (deleting files, running destructive commands, accessing secrets). Guardian only intervenes on that 1%, so you get safety without sacrificing speed.
|
|
14
|
+
|
|
15
|
+
## How It Works
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
AI Agent wants to run a tool (e.g., exec "rm -rf /tmp/data")
|
|
19
|
+
↓
|
|
20
|
+
┌───────────────────────┐
|
|
21
|
+
│ Blacklist Matcher │ ← Keyword rules, 0ms, no model call
|
|
22
|
+
│ critical / warning │
|
|
23
|
+
└───────────┬───────────┘
|
|
24
|
+
↓
|
|
25
|
+
┌───────────────┼───────────────┐
|
|
26
|
+
↓ ↓ ↓
|
|
27
|
+
No match warning critical
|
|
28
|
+
(just go) (1 LLM vote) (3 LLM votes)
|
|
29
|
+
↓ ↓ ↓
|
|
30
|
+
Execute 1 vote check 3 parallel votes
|
|
31
|
+
0ms ~1-2s ~2-4s
|
|
32
|
+
↓ ↓
|
|
33
|
+
confirmed? → ALL 3 confirmed?
|
|
34
|
+
yes: execute yes: execute
|
|
35
|
+
no: block no: block
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Two-Tier Blacklist
|
|
39
|
+
|
|
40
|
+
| Tier | LLM Votes | Threshold | Latency | When |
|
|
41
|
+
|------|-----------|-----------|---------|------|
|
|
42
|
+
| No match | 0 | — | 0ms | Reading files, fetching URLs, normal operations |
|
|
43
|
+
| Warning | 1 | 1/1 | ~1-2s | `sudo`, `rm -r`, `chmod 777`, writing to `.env` |
|
|
44
|
+
| Critical | 3 | 3/3 | ~2-4s | `rm -rf /`, `mkfs`, `dd of=/dev/`, writing to `/etc/passwd` |
|
|
45
|
+
|
|
46
|
+
### What Gets Flagged
|
|
47
|
+
|
|
48
|
+
**Critical** (irreversible destruction or system compromise — needs 3/3 unanimous LLM confirmation):
|
|
49
|
+
|
|
50
|
+
| Pattern | Why |
|
|
51
|
+
|---------|-----|
|
|
52
|
+
| `rm -rf` on system paths | Filesystem destruction |
|
|
53
|
+
| `mkfs`, `dd of=/dev/` | Disk-level destruction |
|
|
54
|
+
| Write to `/etc/passwd`, `/etc/shadow`, `/etc/sudoers` | System auth compromise |
|
|
55
|
+
| `shutdown`, `reboot` | System availability |
|
|
56
|
+
| `curl \| bash`, `base64 -d \| sh` | Remote code execution |
|
|
57
|
+
| `xargs rm`, `find -delete` | Indirect bulk deletion |
|
|
58
|
+
|
|
59
|
+
**Warning** (risky but possibly intentional — needs 1/1 LLM confirmation):
|
|
60
|
+
|
|
61
|
+
| Pattern | Why |
|
|
62
|
+
|---------|-----|
|
|
63
|
+
| `sudo` | Privilege escalation |
|
|
64
|
+
| `rm -r` (non-system paths) | Recursive deletion |
|
|
65
|
+
| `chmod 777`, `chmod -R`, `chown -R` | Dangerous permissions |
|
|
66
|
+
| `kill -9`, `killall`, `pkill` | Force kill processes |
|
|
67
|
+
| `systemctl stop/disable` | Service disruption |
|
|
68
|
+
| `eval` | Arbitrary code execution (review before running) |
|
|
69
|
+
| Write to `.env`, `.ssh/`, `openclaw.json` | Sensitive file modification |
|
|
70
|
+
|
|
71
|
+
### LLM Intent Verification
|
|
72
|
+
|
|
73
|
+
When a blacklist rule matches, Guardian doesn't just block — it reads the recent conversation context and asks a lightweight LLM: **"Did the user explicitly request this operation?"**
|
|
74
|
+
|
|
75
|
+
- Uses the cheapest/fastest model available from your existing OpenClaw config (prefers Haiku, GPT-4o-mini, Gemini Flash)
|
|
76
|
+
- No separate API key needed — piggybacks on whatever you already have configured
|
|
77
|
+
- If LLM is unavailable: critical → block (fail-safe), warning → ask user
|
|
78
|
+
|
|
79
|
+
### Whitelist (Always Allowed)
|
|
80
|
+
|
|
81
|
+
These commands are considered safe and will never be flagged, even if they appear inside a broader command:
|
|
82
|
+
|
|
83
|
+
| Pattern | Why |
|
|
84
|
+
|---------|-----|
|
|
85
|
+
| `mkdir` | Creating directories is non-destructive |
|
|
86
|
+
| `touch` | Creating empty files is non-destructive |
|
|
87
|
+
| `tar`, `unzip`, `gzip`, `gunzip`, `bzip2`, `xz` | Archive extraction/compression — normal dev workflow |
|
|
88
|
+
| `openclaw` (CLI) | OpenClaw's own CLI commands (e.g., `openclaw gateway status`) |
|
|
89
|
+
|
|
90
|
+
Whitelist rules are checked **before** the blacklist. If a command matches a whitelist pattern, it passes through immediately with zero overhead.
|
|
91
|
+
|
|
92
|
+
### Tool-Level Blacklist
|
|
93
|
+
|
|
94
|
+
Guardian doesn't just inspect `exec`, `write`, and `edit` — it also scans tool calls for **any** tool (e.g., `message`, `browser`, database clients, email plugins). To avoid false positives on normal payloads, it only checks action-oriented fields: `action`, `method`, `command`, and `operation` — not the entire parameter object.
|
|
95
|
+
|
|
96
|
+
**Critical** (3/3 unanimous LLM confirmation):
|
|
97
|
+
|
|
98
|
+
| Pattern | Why |
|
|
99
|
+
|---------|-----|
|
|
100
|
+
| `batchDelete`, `expunge`, `emptyTrash`, `purge` | Bulk email deletion — irreversible mailbox destruction |
|
|
101
|
+
| `DROP DATABASE`, `DROP TABLE`, `TRUNCATE`, `DELETE FROM` | Database destruction — irreversible data loss |
|
|
102
|
+
|
|
103
|
+
**Warning** (1/1 LLM confirmation):
|
|
104
|
+
|
|
105
|
+
| Pattern | Why |
|
|
106
|
+
|---------|-----|
|
|
107
|
+
| `delete`, `trash` | Single-item deletion — usually intentional but worth confirming |
|
|
108
|
+
|
|
109
|
+
Everyday operations like `send`, `get`, `web_fetch`, `cron`, `snapshot`, etc. are completely unaffected — they never match any blacklist pattern.
|
|
110
|
+
|
|
111
|
+
### Dual Protection Protocol (双重防护)
|
|
112
|
+
|
|
113
|
+
Guardian provides two layers of protection that work together:
|
|
114
|
+
|
|
115
|
+
**Layer 1 — Guardian Plugin (automatic):** Regex pattern matching + LLM intent verification. When a dangerous operation is detected, Guardian blocks the tool call and returns a rejection message to the agent.
|
|
116
|
+
|
|
117
|
+
**Layer 2 — Agent Self-Discipline (behavioral):** When an agent receives a Guardian block notification, it **must immediately stop**, report the blocked command and reason to the human user, and **wait for explicit confirmation** before proceeding. The agent must not attempt to bypass, retry, or find alternative ways to execute the blocked operation.
|
|
118
|
+
|
|
119
|
+
**The protection chain:**
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
Tool call → Regex match → Guardian blocks → Agent stops → Reports to human → Human decides → Continue or abort
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
This dual approach ensures that even if an agent is determined to perform a dangerous action, it cannot silently retry or work around the block. The human always stays in the loop for any operation Guardian considers risky.
|
|
126
|
+
|
|
127
|
+
#### Recommended AGENTS.md Rule
|
|
128
|
+
|
|
129
|
+
To activate Layer 2, add this rule to your `AGENTS.md` (or equivalent agent instructions file):
|
|
130
|
+
|
|
131
|
+
```markdown
|
|
132
|
+
### Guardian 双重防护协议(硬规则)
|
|
133
|
+
1. **第一层(Guardian 插件)**:regex 初筛 + LLM 意图确认,自动拦截危险操作
|
|
134
|
+
2. **第二层(Agent 自觉)**:当 Guardian 拦截命令时,agent 收到拦截通知后**必须立刻停下**,向用户报告被拦截的命令和原因,等待确认后才能继续。禁止自行绕过、重试或换方式执行被拦截的操作。
|
|
135
|
+
3. **防护链**:regex 初筛 → Guardian 拦截 → agent 停下 → 人类确认 → 继续/放弃
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
This ensures the agent treats Guardian blocks as hard stops rather than soft suggestions.
|
|
139
|
+
|
|
140
|
+
### Why Not Just Use LLMs for Everything?
|
|
141
|
+
|
|
142
|
+
Guardian's blacklist uses **zero-cost keyword rules** — no model calls for pattern matching. Regex like `rm -rf /` → critical, `sudo` → warning is instant and deterministic. LLM verification is only triggered for the ~1% of operations that actually hit the blacklist, and its only job is confirming user intent — not scoring risk.
|
|
143
|
+
|
|
144
|
+
## Quick Start (One Command)
|
|
145
|
+
|
|
146
|
+
### 1. Clone into your OpenClaw workspace
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
cd ~/.openclaw/workspace
|
|
150
|
+
git clone https://github.com/fatcatMaoFei/openclaw-guardian.git
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 2. Register the plugin
|
|
154
|
+
|
|
155
|
+
Add to your `openclaw.json`:
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"plugins": {
|
|
160
|
+
"load": {
|
|
161
|
+
"paths": ["./openclaw-guardian"]
|
|
162
|
+
},
|
|
163
|
+
"entries": {
|
|
164
|
+
"openclaw-guardian": {
|
|
165
|
+
"enabled": true
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### 3. Restart
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
openclaw gateway restart
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
That's it. Guardian is now active. Every tool call goes through blacklist checking automatically.
|
|
179
|
+
|
|
180
|
+
## Customization
|
|
181
|
+
|
|
182
|
+
### Enable / Disable
|
|
183
|
+
|
|
184
|
+
Edit `default-policies.json`:
|
|
185
|
+
|
|
186
|
+
```json
|
|
187
|
+
{
|
|
188
|
+
"enabled": true
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Set to `false` to disable Guardian entirely without uninstalling.
|
|
193
|
+
|
|
194
|
+
### Blacklist Rules
|
|
195
|
+
|
|
196
|
+
The blacklist is defined in `src/blacklist.ts` with two levels of rules:
|
|
197
|
+
|
|
198
|
+
- **`CRITICAL_EXEC` / `CRITICAL_PATH`** — patterns that trigger 3-vote unanimous LLM verification
|
|
199
|
+
- **`WARNING_EXEC` / `WARNING_PATH`** — patterns that trigger 1-vote LLM verification
|
|
200
|
+
- **`SAFE_EXEC`** — whitelisted commands that skip blacklist entirely (e.g., `ls`, `cat`, `git status`)
|
|
201
|
+
|
|
202
|
+
To add your own rules, add a regex + reason to the appropriate array. For example:
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// Add to WARNING_EXEC to flag any docker commands
|
|
206
|
+
{ pattern: /\bdocker\s+(?:rm|rmi|system\s+prune)\b/, reason: "docker resource removal" },
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### LLM Model Selection
|
|
210
|
+
|
|
211
|
+
Guardian automatically picks the cheapest available model from your OpenClaw config. Preference order:
|
|
212
|
+
|
|
213
|
+
1. `claude-haiku-4-5` (Anthropic)
|
|
214
|
+
2. `gpt-4o-mini` (OpenAI)
|
|
215
|
+
3. `gemini-2.0-flash` (Google)
|
|
216
|
+
4. First available model (fallback)
|
|
217
|
+
|
|
218
|
+
No extra configuration needed.
|
|
219
|
+
|
|
220
|
+
## Audit Trail
|
|
221
|
+
|
|
222
|
+
Every blacklist-matched operation is logged to `~/.openclaw/guardian-audit.jsonl` with SHA-256 hash chaining:
|
|
223
|
+
|
|
224
|
+
```json
|
|
225
|
+
{
|
|
226
|
+
"timestamp": "2026-02-24T09:30:00.000Z",
|
|
227
|
+
"toolName": "exec",
|
|
228
|
+
"blacklistLevel": "critical",
|
|
229
|
+
"blacklistReason": "rm -rf on root-level system path",
|
|
230
|
+
"pattern": "rm\\s+(-[a-zA-Z]*r[a-zA-Z]*\\s+|--recursive\\s+)\\/",
|
|
231
|
+
"userConfirmed": false,
|
|
232
|
+
"finalReason": "Only 1/3 confirmed (need 3)",
|
|
233
|
+
"hash": "a1b2c3...",
|
|
234
|
+
"prevHash": "d4e5f6..."
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Tamper-evident: each entry's hash includes the previous entry's hash. Break one link and the whole chain fails verification.
|
|
239
|
+
|
|
240
|
+
## Architecture
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
openclaw-guardian/
|
|
244
|
+
├── openclaw.plugin.json # Plugin manifest (v2.0.0)
|
|
245
|
+
├── index.ts # Entry — registers before_tool_call hook, routes blacklist hits to LLM
|
|
246
|
+
├── src/
|
|
247
|
+
│ ├── blacklist.ts # Two-tier keyword rules (critical/warning), 0ms, no model calls
|
|
248
|
+
│ ├── llm-voter.ts # LLM intent verification (single vote or 3-vote unanimous)
|
|
249
|
+
│ └── audit-log.ts # SHA-256 hash-chain audit logger
|
|
250
|
+
├── default-policies.json # Enable/disable toggle
|
|
251
|
+
├── package.json
|
|
252
|
+
└── tsconfig.json
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### How It Hooks Into OpenClaw
|
|
256
|
+
|
|
257
|
+
OpenClaw's agent loop: `Model → tool_call → Tool Executor → result → Model`
|
|
258
|
+
|
|
259
|
+
Guardian registers a `before_tool_call` plugin hook. This hook fires **after** the model decides to call a tool but **before** the tool actually executes. If Guardian returns `{ block: true }`, the tool is stopped and the model receives a rejection message instead.
|
|
260
|
+
|
|
261
|
+
This is the same hook interface OpenClaw uses internally for loop detection — battle-tested, async-safe, and zero modifications to core code.
|
|
262
|
+
|
|
263
|
+
## Token Cost
|
|
264
|
+
|
|
265
|
+
| Tier | % of Operations | Extra Cost |
|
|
266
|
+
|------|----------------|------------|
|
|
267
|
+
| No match (pass) | ~99% | 0 (no model call) |
|
|
268
|
+
| Warning (1 vote) | ~0.5-1% | ~500 tokens per review |
|
|
269
|
+
| Critical (3 votes) | <0.5% | ~1500 tokens per review |
|
|
270
|
+
|
|
271
|
+
**Average overhead: near zero.** The vast majority of operations never hit the blacklist. When they do, Guardian uses the cheapest model available.
|
|
272
|
+
|
|
273
|
+
## Status
|
|
274
|
+
|
|
275
|
+
🚧 Under active development — contributions welcome.
|
|
276
|
+
|
|
277
|
+
## License
|
|
278
|
+
|
|
279
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw Guardian v2 — Blacklist + LLM Intent Verification
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* tool call → only check exec/write/edit
|
|
6
|
+
* → blacklist match? no → pass (99%)
|
|
7
|
+
* → yes, critical → 3 LLM votes (all must confirm user intent)
|
|
8
|
+
* → yes, warning → 1 LLM vote (confirm user intent)
|
|
9
|
+
* → LLM down → critical: block, warning: ask user
|
|
10
|
+
*/
|
|
11
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
12
|
+
export default function setup(api: OpenClawPluginApi): void;
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AA0B7D,MAAM,CAAC,OAAO,UAAU,KAAK,CAAC,GAAG,EAAE,iBAAiB,GAAG,IAAI,CAsE1D"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw Guardian v2 — Blacklist + LLM Intent Verification
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* tool call → only check exec/write/edit
|
|
6
|
+
* → blacklist match? no → pass (99%)
|
|
7
|
+
* → yes, critical → 3 LLM votes (all must confirm user intent)
|
|
8
|
+
* → yes, warning → 1 LLM vote (confirm user intent)
|
|
9
|
+
* → LLM down → critical: block, warning: ask user
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { join, dirname, resolve, normalize } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
function canonicalizePath(raw) {
|
|
15
|
+
if (!raw)
|
|
16
|
+
return raw;
|
|
17
|
+
// Expand ~ to home dir
|
|
18
|
+
if (raw.startsWith("~/"))
|
|
19
|
+
raw = raw.replace("~", process.env.HOME ?? "/root");
|
|
20
|
+
// Resolve to absolute + normalize (removes ../ etc)
|
|
21
|
+
return normalize(resolve(raw));
|
|
22
|
+
}
|
|
23
|
+
import { checkExecBlacklist, checkPathBlacklist, checkToolBlacklist } from "./src/blacklist.js";
|
|
24
|
+
import { initLlm, singleVote, multiVote } from "./src/llm-voter.js";
|
|
25
|
+
import { initAuditLog, writeAuditEntry } from "./src/audit-log.js";
|
|
26
|
+
function loadEnabled() {
|
|
27
|
+
try {
|
|
28
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const raw = readFileSync(join(dir, "default-policies.json"), "utf-8");
|
|
30
|
+
return JSON.parse(raw).enabled !== false;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return true; // default on if config missing
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export default function setup(api) {
|
|
37
|
+
if (!loadEnabled()) {
|
|
38
|
+
api.logger.info("[guardian] Disabled by policy");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
initAuditLog();
|
|
42
|
+
initLlm(api.config);
|
|
43
|
+
const log = api.logger;
|
|
44
|
+
log.info("[guardian] v2 active — blacklist + LLM intent verification");
|
|
45
|
+
api.on("before_tool_call", async (event, ctx) => {
|
|
46
|
+
const { toolName, params } = event;
|
|
47
|
+
// Only check exec, write, edit — everything else passes instantly
|
|
48
|
+
let match = null;
|
|
49
|
+
if (toolName === "exec") {
|
|
50
|
+
match = checkExecBlacklist((params?.command ?? ""));
|
|
51
|
+
}
|
|
52
|
+
else if (toolName === "write" || toolName === "edit") {
|
|
53
|
+
const rawPath = (params?.file_path ?? params?.path ?? "");
|
|
54
|
+
const safePath = canonicalizePath(rawPath);
|
|
55
|
+
match = checkPathBlacklist(safePath);
|
|
56
|
+
}
|
|
57
|
+
if (!match) {
|
|
58
|
+
// Check tool-level blacklist (covers all other tools like email, message, etc.)
|
|
59
|
+
match = checkToolBlacklist(toolName, (params ?? {}));
|
|
60
|
+
}
|
|
61
|
+
if (!match)
|
|
62
|
+
return; // 99% of calls end here
|
|
63
|
+
const detail = toolName === "exec"
|
|
64
|
+
? (params?.command ?? "").toString().slice(0, 120)
|
|
65
|
+
: (params?.file_path ?? params?.path ?? "").toString().slice(0, 120);
|
|
66
|
+
log.warn(`[guardian] ⚠️ Blacklist hit: ${match.level.toUpperCase()} | tool=${toolName} | ${detail} | rule=${match.reason}`);
|
|
67
|
+
// Blacklist hit — verify user intent via LLM
|
|
68
|
+
const sessionKey = ctx?.sessionKey;
|
|
69
|
+
if (match.level === "critical") {
|
|
70
|
+
const result = await multiVote(toolName, params ?? {}, sessionKey, 3, 3);
|
|
71
|
+
writeAuditEntry(toolName, params ?? {}, match, result.confirmed, result.reason);
|
|
72
|
+
if (!result.confirmed) {
|
|
73
|
+
log.error(`[guardian] 🛑 BLOCKED CRITICAL | tool=${toolName} | ${detail} | votes=${result.reason}`);
|
|
74
|
+
return {
|
|
75
|
+
block: true,
|
|
76
|
+
blockReason: `🛡️ Guardian: 危险操作被拦截 — ${match.reason}。${result.reason}`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
log.info(`[guardian] ✅ CRITICAL passed (3/3 confirmed) | tool=${toolName} | ${detail}`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Warning level: 1 vote
|
|
83
|
+
const result = await singleVote(toolName, params ?? {}, sessionKey);
|
|
84
|
+
writeAuditEntry(toolName, params ?? {}, match, result.confirmed, result.reason);
|
|
85
|
+
if (!result.confirmed) {
|
|
86
|
+
log.warn(`[guardian] 🚫 BLOCKED WARNING | tool=${toolName} | ${detail} | reason=${result.reason}`);
|
|
87
|
+
return {
|
|
88
|
+
block: true,
|
|
89
|
+
blockReason: `🛡️ Guardian: 此操作需要用户确认 — ${match.reason}。请先询问用户是否要执行此操作。`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
log.info(`[guardian] ✅ WARNING passed (user confirmed) | tool=${toolName} | ${detail}`);
|
|
93
|
+
return;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit Log — SHA-256 hash-chain append-only trail.
|
|
3
|
+
* Only logs blacklist-matched operations (not every tool call).
|
|
4
|
+
*/
|
|
5
|
+
import type { BlacklistMatch } from "./blacklist.js";
|
|
6
|
+
export type AuditEntry = {
|
|
7
|
+
timestamp: string;
|
|
8
|
+
toolName: string;
|
|
9
|
+
blacklistLevel: string;
|
|
10
|
+
blacklistReason: string;
|
|
11
|
+
pattern: string;
|
|
12
|
+
userConfirmed: boolean;
|
|
13
|
+
finalReason: string;
|
|
14
|
+
hash: string;
|
|
15
|
+
prevHash: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function initAuditLog(): void;
|
|
18
|
+
export declare function writeAuditEntry(toolName: string, params: Record<string, unknown>, match: BlacklistMatch, userConfirmed: boolean, reason: string): void;
|
|
19
|
+
//# sourceMappingURL=audit-log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-log.d.ts","sourceRoot":"","sources":["../../src/audit-log.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAErD,MAAM,MAAM,UAAU,GAAG;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,OAAO,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AA6BF,wBAAgB,YAAY,IAAI,IAAI,CAKnC;AAED,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,KAAK,EAAE,cAAc,EACrB,aAAa,EAAE,OAAO,EACtB,MAAM,EAAE,MAAM,GACb,IAAI,CAqBN"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit Log — SHA-256 hash-chain append-only trail.
|
|
3
|
+
* Only logs blacklist-matched operations (not every tool call).
|
|
4
|
+
*/
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { appendFileSync, readFileSync, mkdirSync, existsSync } from "node:fs";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
let lastHash = "";
|
|
10
|
+
let logPath = "";
|
|
11
|
+
function getLogPath() {
|
|
12
|
+
if (logPath)
|
|
13
|
+
return logPath;
|
|
14
|
+
logPath = join(homedir(), ".openclaw", "guardian-audit.jsonl");
|
|
15
|
+
return logPath;
|
|
16
|
+
}
|
|
17
|
+
function computeHash(data) {
|
|
18
|
+
return createHash("sha256").update(data).digest("hex");
|
|
19
|
+
}
|
|
20
|
+
function recoverLastHash(path) {
|
|
21
|
+
try {
|
|
22
|
+
if (!existsSync(path))
|
|
23
|
+
return "";
|
|
24
|
+
const content = readFileSync(path, "utf-8").trim();
|
|
25
|
+
if (!content)
|
|
26
|
+
return "";
|
|
27
|
+
const lines = content.split("\n");
|
|
28
|
+
const lastLine = lines[lines.length - 1];
|
|
29
|
+
const entry = JSON.parse(lastLine);
|
|
30
|
+
return entry.hash ?? "";
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function initAuditLog() {
|
|
37
|
+
const path = getLogPath();
|
|
38
|
+
const dir = dirname(path);
|
|
39
|
+
if (!existsSync(dir))
|
|
40
|
+
mkdirSync(dir, { recursive: true });
|
|
41
|
+
lastHash = recoverLastHash(path);
|
|
42
|
+
}
|
|
43
|
+
export function writeAuditEntry(toolName, params, match, userConfirmed, reason) {
|
|
44
|
+
const entry = {
|
|
45
|
+
timestamp: new Date().toISOString(),
|
|
46
|
+
toolName,
|
|
47
|
+
blacklistLevel: match.level,
|
|
48
|
+
blacklistReason: match.reason,
|
|
49
|
+
pattern: match.pattern,
|
|
50
|
+
userConfirmed,
|
|
51
|
+
finalReason: reason,
|
|
52
|
+
prevHash: lastHash,
|
|
53
|
+
};
|
|
54
|
+
const hashInput = JSON.stringify(entry);
|
|
55
|
+
entry.hash = computeHash(hashInput);
|
|
56
|
+
lastHash = entry.hash;
|
|
57
|
+
try {
|
|
58
|
+
appendFileSync(getLogPath(), JSON.stringify(entry) + "\n", "utf-8");
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
console.error(`[guardian] audit write failed: ${err}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=audit-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-log.js","sourceRoot":"","sources":["../../src/audit-log.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC9E,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAelC,IAAI,QAAQ,GAAG,EAAE,CAAC;AAClB,IAAI,OAAO,GAAG,EAAE,CAAC;AAEjB,SAAS,UAAU;IACjB,IAAI,OAAO;QAAE,OAAO,OAAO,CAAC;IAC5B,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,WAAW,EAAE,sBAAsB,CAAC,CAAC;IAC/D,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,WAAW,CAAC,IAAY;IAC/B,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACzD,CAAC;AAED,SAAS,eAAe,CAAC,IAAY;IACnC,IAAI,CAAC;QACH,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,EAAE,CAAC;QACjC,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;QACnD,IAAI,CAAC,OAAO;YAAE,OAAO,EAAE,CAAC;QACxB,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAClC,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACzC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAe,CAAC;QACjD,OAAO,KAAK,CAAC,IAAI,IAAI,EAAE,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,YAAY;IAC1B,MAAM,IAAI,GAAG,UAAU,EAAE,CAAC;IAC1B,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1D,QAAQ,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,eAAe,CAC7B,QAAgB,EAChB,MAA+B,EAC/B,KAAqB,EACrB,aAAsB,EACtB,MAAc;IAEd,MAAM,KAAK,GAAiD;QAC1D,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,QAAQ;QACR,cAAc,EAAE,KAAK,CAAC,KAAK;QAC3B,eAAe,EAAE,KAAK,CAAC,MAAM;QAC7B,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,aAAa;QACb,WAAW,EAAE,MAAM;QACnB,QAAQ,EAAE,QAAQ;KACnB,CAAC;IAEF,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACxC,KAAK,CAAC,IAAI,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC;IACpC,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC;IAEtB,IAAI,CAAC;QACH,cAAc,CAAC,UAAU,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,IAAI,EAAE,OAAO,CAAC,CAAC;IACtE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,kCAAkC,GAAG,EAAE,CAAC,CAAC;IACzD,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blacklist rules — pure pattern matching, no LLM involved.
|
|
3
|
+
* Two levels: "critical" (needs 3/3 LLM votes) and "warning" (needs 1/1).
|
|
4
|
+
*
|
|
5
|
+
* IMPORTANT: These patterns are checked against:
|
|
6
|
+
* - exec: the command string
|
|
7
|
+
* - write/edit: the file path
|
|
8
|
+
* The caller (index.ts) decides what text to pass in.
|
|
9
|
+
*/
|
|
10
|
+
export type BlacklistMatch = {
|
|
11
|
+
level: "critical" | "warning";
|
|
12
|
+
pattern: string;
|
|
13
|
+
reason: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Check a command (exec) against blacklist.
|
|
17
|
+
* Splits on shell operators and checks each segment.
|
|
18
|
+
* Returns null if no match (99% of calls).
|
|
19
|
+
*/
|
|
20
|
+
export declare function checkExecBlacklist(command: string): BlacklistMatch | null;
|
|
21
|
+
/**
|
|
22
|
+
* Check a file path (write/edit) against blacklist.
|
|
23
|
+
* Returns null if no match.
|
|
24
|
+
*/
|
|
25
|
+
export declare function checkPathBlacklist(filePath: string): BlacklistMatch | null;
|
|
26
|
+
/**
|
|
27
|
+
* Check any tool call's params against tool-level blacklist.
|
|
28
|
+
* Only checks specific param fields (not full serialization) to avoid false positives.
|
|
29
|
+
* Skips exec/write/edit (already handled by dedicated checkers).
|
|
30
|
+
* Returns null if no match.
|
|
31
|
+
*/
|
|
32
|
+
export declare function checkToolBlacklist(toolName: string, params: Record<string, unknown>): BlacklistMatch | null;
|
|
33
|
+
//# sourceMappingURL=blacklist.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blacklist.d.ts","sourceRoot":"","sources":["../../src/blacklist.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,UAAU,GAAG,SAAS,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAiLF;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CA4BzE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAI1E;AAqBD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,cAAc,GAAG,IAAI,CAmB3G"}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blacklist rules — pure pattern matching, no LLM involved.
|
|
3
|
+
* Two levels: "critical" (needs 3/3 LLM votes) and "warning" (needs 1/1).
|
|
4
|
+
*
|
|
5
|
+
* IMPORTANT: These patterns are checked against:
|
|
6
|
+
* - exec: the command string
|
|
7
|
+
* - write/edit: the file path
|
|
8
|
+
* The caller (index.ts) decides what text to pass in.
|
|
9
|
+
*/
|
|
10
|
+
// ── CRITICAL: irreversible destruction or system compromise ────────
|
|
11
|
+
// Needs 3/3 LLM votes confirming user intent to pass
|
|
12
|
+
const CRITICAL_EXEC = [
|
|
13
|
+
// Filesystem destruction — only recursive rm on system paths
|
|
14
|
+
{ pattern: /rm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+|--recursive\s+)\/(?!tmp\/|home\/clawdbot\/)/, reason: "rm -rf on root-level system path" },
|
|
15
|
+
{ pattern: /rm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+|--recursive\s+)~\//, reason: "rm -rf on home directory" },
|
|
16
|
+
{ pattern: /mkfs\b/, reason: "filesystem format (mkfs)" },
|
|
17
|
+
{ pattern: /dd\s+if=.*of=\/dev\//, reason: "raw disk write (dd)" },
|
|
18
|
+
{ pattern: />\s*\/dev\/sd/, reason: "redirect to block device" },
|
|
19
|
+
// System auth files (write/modify, not read)
|
|
20
|
+
{ pattern: /(?:tee|>>?)\s*\/etc\/(?:passwd|shadow|sudoers)/, reason: "write to system auth file" },
|
|
21
|
+
{ pattern: /sed\s+-i.*\/etc\/(?:passwd|shadow|sudoers)/, reason: "in-place edit of system auth file" },
|
|
22
|
+
// System shutdown
|
|
23
|
+
{ pattern: /\b(?:shutdown|reboot)\b/, reason: "system shutdown/reboot" },
|
|
24
|
+
{ pattern: /\binit\s+[06]\b/, reason: "system halt/reboot (init)" },
|
|
25
|
+
// Kill SSH (locks out remote access)
|
|
26
|
+
{ pattern: /systemctl\s+(?:stop|disable)\s+sshd/, reason: "disable SSH (remote lockout)" },
|
|
27
|
+
// === BYPASS PREVENTION ===
|
|
28
|
+
// Absolute path to rm
|
|
29
|
+
{ pattern: /\/bin\/rm\s+(-[a-zA-Z]*r[a-zA-Z]*)\s+/, reason: "rm via absolute path" },
|
|
30
|
+
{ pattern: /\/usr\/bin\/rm\s+(-[a-zA-Z]*r[a-zA-Z]*)\s+/, reason: "rm via absolute path" },
|
|
31
|
+
// (eval, perl -e, ruby -e moved to WARNING)
|
|
32
|
+
// xargs with dangerous commands
|
|
33
|
+
{ pattern: /xargs\s+.*\brm\b/, reason: "xargs rm (indirect deletion)" },
|
|
34
|
+
{ pattern: /xargs\s+.*\bchmod\b/, reason: "xargs chmod (indirect permission change)" },
|
|
35
|
+
// find -exec with dangerous commands
|
|
36
|
+
{ pattern: /find\s+.*-exec\s+.*\brm\b/, reason: "find -exec rm (indirect deletion)" },
|
|
37
|
+
{ pattern: /find\s+.*-delete\b/, reason: "find -delete (bulk deletion)" },
|
|
38
|
+
];
|
|
39
|
+
const CRITICAL_PATH = [
|
|
40
|
+
{ pattern: /^\/etc\/(?:passwd|shadow|sudoers)$/, reason: "write to system auth file" },
|
|
41
|
+
{ pattern: /^\/boot\//, reason: "write to boot partition" },
|
|
42
|
+
];
|
|
43
|
+
// ── WARNING: risky but possibly intentional ────────────────────────
|
|
44
|
+
// Needs 1/1 LLM vote confirming user intent to pass
|
|
45
|
+
const WARNING_EXEC = [
|
|
46
|
+
// Recursive delete (non-system paths — CRITICAL already catches system paths)
|
|
47
|
+
{ pattern: /rm\s+(-[a-zA-Z]*r[a-zA-Z]*)\s+/, reason: "recursive file deletion" },
|
|
48
|
+
// Privilege escalation
|
|
49
|
+
{ pattern: /\bsudo\s+/, reason: "privilege escalation (sudo)" },
|
|
50
|
+
// Dangerous permissions
|
|
51
|
+
{ pattern: /chmod\s+[47]77\b/, reason: "world-writable permission (chmod 777)" },
|
|
52
|
+
{ pattern: /chmod\s+-R\s+/, reason: "recursive permission change" },
|
|
53
|
+
{ pattern: /chown\s+-R\s+/, reason: "recursive ownership change" },
|
|
54
|
+
// Force kill
|
|
55
|
+
{ pattern: /kill\s+-9\s+/, reason: "force kill process (SIGKILL)" },
|
|
56
|
+
{ pattern: /\bkillall\s+/, reason: "killall processes" },
|
|
57
|
+
{ pattern: /\bpkill\s+/, reason: "pkill processes" },
|
|
58
|
+
// Service management (only stop/disable — restart is less dangerous)
|
|
59
|
+
{ pattern: /systemctl\s+(?:stop|disable)\s+/, reason: "systemctl stop/disable service" },
|
|
60
|
+
// Database destruction
|
|
61
|
+
{ pattern: /DROP\s+(?:DATABASE|TABLE)\b/i, reason: "DROP DATABASE/TABLE" },
|
|
62
|
+
{ pattern: /TRUNCATE\s+/i, reason: "TRUNCATE table" },
|
|
63
|
+
// Interpreter inline execution (only check eval — python/node/perl/ruby are normal dev tools)
|
|
64
|
+
{ pattern: /\beval\s+/, reason: "eval execution (arbitrary code)" },
|
|
65
|
+
// Network/firewall changes
|
|
66
|
+
{ pattern: /\biptables\s+/, reason: "firewall rule change (iptables)" },
|
|
67
|
+
{ pattern: /\bufw\s+(?:allow|deny|delete|disable)\b/, reason: "firewall rule change (ufw)" },
|
|
68
|
+
// Crontab modification
|
|
69
|
+
{ pattern: /\bcrontab\s+(-r|-e)\b/, reason: "crontab modification" },
|
|
70
|
+
// Disk operations
|
|
71
|
+
{ pattern: /\bfdisk\s+/, reason: "disk partition operation" },
|
|
72
|
+
{ pattern: /\bparted\s+/, reason: "disk partition operation" },
|
|
73
|
+
{ pattern: /\bmount\s+/, reason: "filesystem mount operation" },
|
|
74
|
+
{ pattern: /\bumount\s+/, reason: "filesystem unmount operation" },
|
|
75
|
+
// SSH key operations
|
|
76
|
+
{ pattern: /ssh-keygen\s+/, reason: "SSH key generation/modification" },
|
|
77
|
+
// Environment variable manipulation that could affect security
|
|
78
|
+
{ pattern: /export\s+(?:PATH|LD_PRELOAD|LD_LIBRARY_PATH)=/, reason: "security-sensitive environment variable change" },
|
|
79
|
+
];
|
|
80
|
+
const WARNING_PATH = [
|
|
81
|
+
{ pattern: /^\/etc\//, reason: "write to /etc/ system config" },
|
|
82
|
+
{ pattern: /^\/root\//, reason: "write to /root/ directory" },
|
|
83
|
+
];
|
|
84
|
+
// ── Safe Command Patterns (whitelist, checked before blacklist) ─────
|
|
85
|
+
const SAFE_EXEC = [
|
|
86
|
+
// git rm --cached only removes from index, not filesystem
|
|
87
|
+
/^git\s+rm\s+.*--cached/,
|
|
88
|
+
// git operations are generally safe
|
|
89
|
+
/^git\s+(?:add|commit|push|pull|fetch|log|status|diff|branch|checkout|merge|rebase|stash|tag|remote|clone)\b/,
|
|
90
|
+
// echo/printf — ONLY safe if not piped to shell (pipe splits into separate segments)
|
|
91
|
+
/^(?:echo|printf)\s+/,
|
|
92
|
+
// read-only commands — exclude find (can be used with -exec/-delete)
|
|
93
|
+
/^(?:cat|head|tail|less|more|grep|ls|stat|file|wc|du|df|which|whereis|type|id|whoami|hostname|uname|date|uptime)\s*/,
|
|
94
|
+
// package info (not install)
|
|
95
|
+
/^(?:apt|dpkg|pip|npm)\s+(?:list|show|info|search)\b/,
|
|
96
|
+
// safe file operations (create only, no overwrite risk)
|
|
97
|
+
/^(?:mkdir|touch)\s+/,
|
|
98
|
+
// archive/compression (read-heavy, low risk)
|
|
99
|
+
/^(?:tar|unzip|gzip|gunzip|bzip2|xz|7z)\s+/,
|
|
100
|
+
// openclaw CLI
|
|
101
|
+
/^openclaw\s+/,
|
|
102
|
+
];
|
|
103
|
+
// ── Quote/Comment Detection ────────────────────────────────────────
|
|
104
|
+
function isQuotedOrCommented(text, matchIndex) {
|
|
105
|
+
const before = text.slice(0, matchIndex);
|
|
106
|
+
// Inside double quotes?
|
|
107
|
+
const doubleQuotes = (before.match(/"/g) || []).length;
|
|
108
|
+
if (doubleQuotes % 2 === 1)
|
|
109
|
+
return true;
|
|
110
|
+
// Inside single quotes?
|
|
111
|
+
const singleQuotes = (before.match(/'/g) || []).length;
|
|
112
|
+
if (singleQuotes % 2 === 1)
|
|
113
|
+
return true;
|
|
114
|
+
// After a comment character on the same line?
|
|
115
|
+
const lastNewline = before.lastIndexOf("\n");
|
|
116
|
+
const currentLine = before.slice(lastNewline + 1);
|
|
117
|
+
if (currentLine.includes("#"))
|
|
118
|
+
return true;
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
// ── Matching ───────────────────────────────────────────────────────
|
|
122
|
+
function matchRules(text, rules, level) {
|
|
123
|
+
for (const rule of rules) {
|
|
124
|
+
const m = rule.pattern.exec(text);
|
|
125
|
+
if (m && !isQuotedOrCommented(text, m.index)) {
|
|
126
|
+
return { level, pattern: rule.pattern.source, reason: rule.reason };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
// ── Command Segmentation ───────────────────────────────────────────
|
|
132
|
+
function splitCommand(cmd) {
|
|
133
|
+
// Split on shell operators, but not inside quotes
|
|
134
|
+
const segments = [];
|
|
135
|
+
let current = "";
|
|
136
|
+
let inSingle = false;
|
|
137
|
+
let inDouble = false;
|
|
138
|
+
for (let i = 0; i < cmd.length; i++) {
|
|
139
|
+
const ch = cmd[i];
|
|
140
|
+
if (ch === "'" && !inDouble) {
|
|
141
|
+
inSingle = !inSingle;
|
|
142
|
+
current += ch;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (ch === '"' && !inSingle) {
|
|
146
|
+
inDouble = !inDouble;
|
|
147
|
+
current += ch;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (!inSingle && !inDouble) {
|
|
151
|
+
// Check double-char operators first: && and ||
|
|
152
|
+
if ((ch === '&' && cmd[i + 1] === '&') || (ch === '|' && cmd[i + 1] === '|')) {
|
|
153
|
+
segments.push(current.trim());
|
|
154
|
+
current = "";
|
|
155
|
+
i++; // skip second char
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
// Single-char separators: ; | \n
|
|
159
|
+
if (ch === ';' || ch === '|' || ch === '\n') {
|
|
160
|
+
segments.push(current.trim());
|
|
161
|
+
current = "";
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
current += ch;
|
|
166
|
+
}
|
|
167
|
+
if (current.trim())
|
|
168
|
+
segments.push(current.trim());
|
|
169
|
+
return segments.filter(Boolean);
|
|
170
|
+
}
|
|
171
|
+
// ── Public API ─────────────────────────────────────────────────────
|
|
172
|
+
/**
|
|
173
|
+
* Check a command (exec) against blacklist.
|
|
174
|
+
* Splits on shell operators and checks each segment.
|
|
175
|
+
* Returns null if no match (99% of calls).
|
|
176
|
+
*/
|
|
177
|
+
export function checkExecBlacklist(command) {
|
|
178
|
+
if (!command)
|
|
179
|
+
return null;
|
|
180
|
+
// Phase 1: Check the FULL command string for pipe-based attacks
|
|
181
|
+
// These patterns span across pipe boundaries and must be checked before splitting
|
|
182
|
+
const PIPE_ATTACKS = [
|
|
183
|
+
{ pattern: /base64\s+(-d|--decode).*\|\s*(?:bash|sh|zsh|dash)/, reason: "base64 decoded pipe to shell" },
|
|
184
|
+
{ pattern: /\bcurl\b.*\|\s*(?:bash|sh|zsh|dash|python|perl|ruby)/, reason: "curl pipe to shell (remote code execution)" },
|
|
185
|
+
{ pattern: /\bwget\b.*\|\s*(?:bash|sh|zsh|dash|python|perl|ruby)/, reason: "wget pipe to shell (remote code execution)" },
|
|
186
|
+
{ pattern: /\becho\b.*\|\s*(?:bash|sh|zsh|dash)\b/, reason: "echo pipe to shell" },
|
|
187
|
+
{ pattern: /\bprintf\b.*\|\s*(?:bash|sh|zsh|dash)\b/, reason: "printf pipe to shell" },
|
|
188
|
+
{ pattern: /\|\s*(?:bash|sh|zsh|dash)\s*$/, reason: "pipe to shell interpreter" },
|
|
189
|
+
{ pattern: /\|\s*(?:bash|sh|zsh|dash)\s*[;&|]/, reason: "pipe to shell interpreter" },
|
|
190
|
+
];
|
|
191
|
+
const fullMatch = matchRules(command, PIPE_ATTACKS, "critical");
|
|
192
|
+
if (fullMatch)
|
|
193
|
+
return fullMatch;
|
|
194
|
+
// Phase 2: Split on shell operators and check each segment
|
|
195
|
+
const segments = splitCommand(command);
|
|
196
|
+
for (const seg of segments) {
|
|
197
|
+
// Whitelist check: safe commands skip blacklist entirely
|
|
198
|
+
if (SAFE_EXEC.some(re => re.test(seg)))
|
|
199
|
+
continue;
|
|
200
|
+
const m = matchRules(seg, CRITICAL_EXEC, "critical")
|
|
201
|
+
?? matchRules(seg, WARNING_EXEC, "warning");
|
|
202
|
+
if (m)
|
|
203
|
+
return m;
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Check a file path (write/edit) against blacklist.
|
|
209
|
+
* Returns null if no match.
|
|
210
|
+
*/
|
|
211
|
+
export function checkPathBlacklist(filePath) {
|
|
212
|
+
if (!filePath)
|
|
213
|
+
return null;
|
|
214
|
+
return matchRules(filePath, CRITICAL_PATH, "critical")
|
|
215
|
+
?? matchRules(filePath, WARNING_PATH, "warning");
|
|
216
|
+
}
|
|
217
|
+
const TOOL_RULES = [
|
|
218
|
+
// Email: bulk delete / trash / expunge (irreversible)
|
|
219
|
+
{ tool: /.*/, param: "*", pattern: /\b(?:batchDelete|expunge|emptyTrash|purge)\b/i, level: "critical", reason: "bulk email deletion (irreversible)" },
|
|
220
|
+
// Email: single delete/trash (only matches action field value)
|
|
221
|
+
{ tool: /.*/, param: "*", pattern: /\b(?:delete|trash)\b/i, level: "warning", reason: "email/message deletion" },
|
|
222
|
+
// Destructive database queries embedded in tool params
|
|
223
|
+
{ tool: /.*/, param: "*", pattern: /\b(?:DROP\s+(?:DATABASE|TABLE)|TRUNCATE\s+|DELETE\s+FROM)\b/i, level: "critical", reason: "destructive database query in tool params" },
|
|
224
|
+
];
|
|
225
|
+
/**
|
|
226
|
+
* Check any tool call's params against tool-level blacklist.
|
|
227
|
+
* Only checks specific param fields (not full serialization) to avoid false positives.
|
|
228
|
+
* Skips exec/write/edit (already handled by dedicated checkers).
|
|
229
|
+
* Returns null if no match.
|
|
230
|
+
*/
|
|
231
|
+
export function checkToolBlacklist(toolName, params) {
|
|
232
|
+
// exec/write/edit already have dedicated checkers
|
|
233
|
+
if (toolName === "exec" || toolName === "write" || toolName === "edit")
|
|
234
|
+
return null;
|
|
235
|
+
if (!params || Object.keys(params).length === 0)
|
|
236
|
+
return null;
|
|
237
|
+
// Only check action-like fields for all rules
|
|
238
|
+
const actionFields = ["action", "method", "command", "operation"];
|
|
239
|
+
const actionValue = actionFields
|
|
240
|
+
.map(f => typeof params[f] === "string" ? params[f] : "")
|
|
241
|
+
.filter(Boolean)
|
|
242
|
+
.join(" ")
|
|
243
|
+
.toLowerCase();
|
|
244
|
+
for (const rule of TOOL_RULES) {
|
|
245
|
+
if (!rule.tool.test(toolName))
|
|
246
|
+
continue;
|
|
247
|
+
const m = rule.pattern.exec(actionValue);
|
|
248
|
+
if (m)
|
|
249
|
+
return { level: rule.level, pattern: rule.pattern.source, reason: rule.reason };
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
//# sourceMappingURL=blacklist.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blacklist.js","sourceRoot":"","sources":["../../src/blacklist.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM voter — calls a lightweight model to check if user explicitly requested/confirmed
|
|
3
|
+
* the flagged operation. Single job: "Did the user ask for this?"
|
|
4
|
+
*
|
|
5
|
+
* LLM config is read from OpenClaw's own model provider config (api.config.models.providers).
|
|
6
|
+
* No separate API key needed — uses whatever the user already configured.
|
|
7
|
+
*/
|
|
8
|
+
export type Vote = {
|
|
9
|
+
voter: number;
|
|
10
|
+
confirmed: boolean;
|
|
11
|
+
reason: string;
|
|
12
|
+
};
|
|
13
|
+
export type VoteResult = {
|
|
14
|
+
confirmed: boolean;
|
|
15
|
+
reason: string;
|
|
16
|
+
votes?: Vote[];
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Initialize LLM config from OpenClaw's provider config.
|
|
20
|
+
* Called once at plugin setup.
|
|
21
|
+
*/
|
|
22
|
+
export declare function initLlm(config: Record<string, unknown>): void;
|
|
23
|
+
export declare function readRecentContext(_sessionKey?: string): string;
|
|
24
|
+
export declare function singleVote(toolName: string, params: Record<string, any>, sessionKey?: string): Promise<VoteResult>;
|
|
25
|
+
export declare function multiVote(toolName: string, params: Record<string, any>, sessionKey?: string, count?: number, threshold?: number): Promise<VoteResult & {
|
|
26
|
+
votes: Vote[];
|
|
27
|
+
}>;
|
|
28
|
+
//# sourceMappingURL=llm-voter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"llm-voter.d.ts","sourceRoot":"","sources":["../../src/llm-voter.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,MAAM,MAAM,IAAI,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AACzE,MAAM,MAAM,UAAU,GAAG;IAAE,SAAS,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,IAAI,EAAE,CAAA;CAAE,CAAC;AAuBhF;;;GAGG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CA6C7D;AA0ED,wBAAgB,iBAAiB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAmC9D;AAqFD,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,UAAU,CAAC,EAAE,MAAM,GACjE,OAAO,CAAC,UAAU,CAAC,CAQrB;AAED,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,UAAU,CAAC,EAAE,MAAM,EAClE,KAAK,SAAI,EAAE,SAAS,SAAI,GACvB,OAAO,CAAC,UAAU,GAAG;IAAE,KAAK,EAAE,IAAI,EAAE,CAAA;CAAE,CAAC,CAkBzC"}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM voter — calls a lightweight model to check if user explicitly requested/confirmed
|
|
3
|
+
* the flagged operation. Single job: "Did the user ask for this?"
|
|
4
|
+
*
|
|
5
|
+
* LLM config is read from OpenClaw's own model provider config (api.config.models.providers).
|
|
6
|
+
* No separate API key needed — uses whatever the user already configured.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
// ── LLM Config (resolved at init from OpenClaw config) ─────────────
|
|
11
|
+
let llmUrl = "";
|
|
12
|
+
let llmApiKey = "";
|
|
13
|
+
let llmModel = "";
|
|
14
|
+
let llmApi = "anthropic-messages";
|
|
15
|
+
let llmHeaders = {};
|
|
16
|
+
let llmReady = false;
|
|
17
|
+
const LLM_TIMEOUT_MS = 5000;
|
|
18
|
+
const LLM_MAX_TOKENS = 200;
|
|
19
|
+
// Preferred models for guardian voting (cheap + fast)
|
|
20
|
+
const PREFERRED_MODELS = [
|
|
21
|
+
"claude-haiku-4-5-20251001",
|
|
22
|
+
"claude-3-5-haiku",
|
|
23
|
+
"claude-3-haiku",
|
|
24
|
+
"gpt-4o-mini",
|
|
25
|
+
"gemini-2.0-flash",
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* Initialize LLM config from OpenClaw's provider config.
|
|
29
|
+
* Called once at plugin setup.
|
|
30
|
+
*/
|
|
31
|
+
export function initLlm(config) {
|
|
32
|
+
const providers = config?.models?.providers;
|
|
33
|
+
if (!providers || typeof providers !== "object") {
|
|
34
|
+
console.error("[guardian] No model providers found in config");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
// Strategy: find a provider with a cheap/fast model
|
|
38
|
+
for (const preferred of PREFERRED_MODELS) {
|
|
39
|
+
for (const [, provider] of Object.entries(providers)) {
|
|
40
|
+
if (!provider.baseUrl || !provider.apiKey)
|
|
41
|
+
continue;
|
|
42
|
+
const models = provider.models ?? [];
|
|
43
|
+
const found = models.find((m) => m.id === preferred || m.id?.includes(preferred) || m.name?.includes(preferred));
|
|
44
|
+
if (found) {
|
|
45
|
+
llmUrl = provider.baseUrl.replace(/\/$/, "");
|
|
46
|
+
llmApiKey = provider.apiKey;
|
|
47
|
+
llmModel = found.id;
|
|
48
|
+
llmApi = found.api ?? provider.api ?? "anthropic-messages";
|
|
49
|
+
llmHeaders = { ...provider.headers, ...found.headers };
|
|
50
|
+
llmReady = true;
|
|
51
|
+
console.log(`[guardian] LLM ready: ${llmModel} via ${llmUrl}`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Fallback: use the first provider with any model
|
|
57
|
+
for (const [, provider] of Object.entries(providers)) {
|
|
58
|
+
if (!provider.baseUrl || !provider.apiKey)
|
|
59
|
+
continue;
|
|
60
|
+
const models = provider.models ?? [];
|
|
61
|
+
if (models.length > 0) {
|
|
62
|
+
llmUrl = provider.baseUrl.replace(/\/$/, "");
|
|
63
|
+
llmApiKey = provider.apiKey;
|
|
64
|
+
llmModel = models[0].id;
|
|
65
|
+
llmApi = models[0].api ?? provider.api ?? "anthropic-messages";
|
|
66
|
+
llmHeaders = { ...provider.headers, ...models[0].headers };
|
|
67
|
+
llmReady = true;
|
|
68
|
+
console.log(`[guardian] LLM fallback: ${llmModel} via ${llmUrl}`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
console.error("[guardian] No usable LLM provider found");
|
|
73
|
+
}
|
|
74
|
+
// ── System Prompt ──────────────────────────────────────────────────
|
|
75
|
+
const SYSTEM_PROMPT = `You are a security confirmation checker for an AI agent.
|
|
76
|
+
|
|
77
|
+
Your ONLY job is to determine: Did the user explicitly request or confirm this operation?
|
|
78
|
+
|
|
79
|
+
You will receive:
|
|
80
|
+
1. A tool call that was flagged as potentially dangerous
|
|
81
|
+
2. Recent conversation messages for context
|
|
82
|
+
|
|
83
|
+
Rules:
|
|
84
|
+
- If the user clearly asked for this operation (e.g., "delete that folder", "remove the old files", "restart the service"), answer YES.
|
|
85
|
+
- If the user confirmed after being asked (e.g., "yes", "do it", "confirmed", "go ahead"), answer YES.
|
|
86
|
+
- If there is no clear user intent or confirmation for this specific operation, answer NO.
|
|
87
|
+
- When in doubt, answer NO.
|
|
88
|
+
- Do NOT evaluate whether the operation is dangerous — that's already been determined. You are ONLY checking user intent.
|
|
89
|
+
|
|
90
|
+
Examples:
|
|
91
|
+
|
|
92
|
+
User said: "帮我删掉 /data/old-backup 这个文件夹"
|
|
93
|
+
Tool: exec, Command: rm -rf /data/old-backup
|
|
94
|
+
→ {"confirmed": true, "reason": "User explicitly asked to delete /data/old-backup"}
|
|
95
|
+
|
|
96
|
+
User said: "看看磁盘空间"
|
|
97
|
+
Tool: exec, Command: rm -rf /var/log/old
|
|
98
|
+
→ {"confirmed": false, "reason": "User asked about disk space, not file deletion"}
|
|
99
|
+
|
|
100
|
+
User said: "好的,执行吧"
|
|
101
|
+
Tool: exec, Command: sudo systemctl restart nginx
|
|
102
|
+
→ {"confirmed": true, "reason": "User said go ahead, confirming the operation"}
|
|
103
|
+
|
|
104
|
+
User said: "我要重启服务器"
|
|
105
|
+
Tool: exec, Command: reboot
|
|
106
|
+
→ {"confirmed": true, "reason": "User explicitly requested server reboot"}
|
|
107
|
+
|
|
108
|
+
User said: "检查一下 nginx 状态"
|
|
109
|
+
Tool: exec, Command: systemctl stop nginx
|
|
110
|
+
→ {"confirmed": false, "reason": "User asked to check status, not stop the service"}
|
|
111
|
+
|
|
112
|
+
User said: "yes"
|
|
113
|
+
Tool: exec, Command: rm -rf /home/user/project
|
|
114
|
+
→ {"confirmed": true, "reason": "User confirmed with yes"}
|
|
115
|
+
|
|
116
|
+
User said: (no recent messages)
|
|
117
|
+
Tool: exec, Command: rm -rf /tmp/cache
|
|
118
|
+
→ {"confirmed": false, "reason": "No user messages found to confirm this operation"}
|
|
119
|
+
|
|
120
|
+
User said: "echo 'rm -rf /' 这个命令很危险"
|
|
121
|
+
Tool: exec, Command: rm -rf /
|
|
122
|
+
→ {"confirmed": false, "reason": "User was discussing the command as dangerous, not requesting execution"}
|
|
123
|
+
|
|
124
|
+
Respond with EXACTLY one JSON object:
|
|
125
|
+
{"confirmed": true/false, "reason": "brief explanation"}`;
|
|
126
|
+
// ── Context Reader ─────────────────────────────────────────────────
|
|
127
|
+
function resolveSessionsDir() {
|
|
128
|
+
// Try common paths
|
|
129
|
+
const candidates = [
|
|
130
|
+
join(process.env.HOME ?? "/root", ".openclaw/agents/main/sessions"),
|
|
131
|
+
"/root/.openclaw/agents/main/sessions",
|
|
132
|
+
"/home/clawdbot/.openclaw/agents/main/sessions",
|
|
133
|
+
];
|
|
134
|
+
for (const dir of candidates) {
|
|
135
|
+
try {
|
|
136
|
+
readdirSync(dir);
|
|
137
|
+
return dir;
|
|
138
|
+
}
|
|
139
|
+
catch { /* try next */ }
|
|
140
|
+
}
|
|
141
|
+
return candidates[0]; // fallback
|
|
142
|
+
}
|
|
143
|
+
export function readRecentContext(_sessionKey) {
|
|
144
|
+
try {
|
|
145
|
+
const sessDir = resolveSessionsDir();
|
|
146
|
+
const files = readdirSync(sessDir)
|
|
147
|
+
.filter((f) => f.endsWith(".jsonl"))
|
|
148
|
+
.map((f) => ({ name: f, mtime: statSync(join(sessDir, f)).mtimeMs }))
|
|
149
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
150
|
+
if (files.length === 0)
|
|
151
|
+
return "(no session context available)";
|
|
152
|
+
const latest = join(sessDir, files[0].name);
|
|
153
|
+
const raw = readFileSync(latest, "utf-8");
|
|
154
|
+
const lines = raw.split("\n").slice(-50).join("\n");
|
|
155
|
+
const userMessages = [];
|
|
156
|
+
for (const line of lines.split("\n")) {
|
|
157
|
+
if (!line.trim())
|
|
158
|
+
continue;
|
|
159
|
+
try {
|
|
160
|
+
const entry = JSON.parse(line);
|
|
161
|
+
const msg = entry.message ?? entry;
|
|
162
|
+
if (msg.role === "user") {
|
|
163
|
+
const text = typeof msg.content === "string"
|
|
164
|
+
? msg.content
|
|
165
|
+
: Array.isArray(msg.content)
|
|
166
|
+
? msg.content.filter((b) => b.type === "text").map((b) => b.text).join(" ")
|
|
167
|
+
: "";
|
|
168
|
+
if (text.trim())
|
|
169
|
+
userMessages.push(text.trim().slice(0, 500));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch { /* skip malformed lines */ }
|
|
173
|
+
}
|
|
174
|
+
return userMessages.slice(-3).join("\n---\n") || "(no user messages found)";
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return "(failed to read session context)";
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// ── LLM Call (supports anthropic-messages and openai-completions) ──
|
|
181
|
+
async function callLLM(userPrompt) {
|
|
182
|
+
if (!llmReady)
|
|
183
|
+
throw new Error("LLM not initialized");
|
|
184
|
+
const controller = new AbortController();
|
|
185
|
+
const timer = setTimeout(() => controller.abort(), LLM_TIMEOUT_MS);
|
|
186
|
+
try {
|
|
187
|
+
let resp;
|
|
188
|
+
if (llmApi === "anthropic-messages") {
|
|
189
|
+
const endpoint = llmUrl.endsWith("/messages") ? llmUrl : `${llmUrl}/v1/messages`;
|
|
190
|
+
resp = await fetch(endpoint, {
|
|
191
|
+
method: "POST",
|
|
192
|
+
headers: {
|
|
193
|
+
"Content-Type": "application/json",
|
|
194
|
+
"x-api-key": llmApiKey,
|
|
195
|
+
"anthropic-version": "2023-06-01",
|
|
196
|
+
...llmHeaders,
|
|
197
|
+
},
|
|
198
|
+
body: JSON.stringify({
|
|
199
|
+
model: llmModel,
|
|
200
|
+
max_tokens: LLM_MAX_TOKENS,
|
|
201
|
+
temperature: 0,
|
|
202
|
+
system: SYSTEM_PROMPT,
|
|
203
|
+
messages: [{ role: "user", content: userPrompt }],
|
|
204
|
+
}),
|
|
205
|
+
signal: controller.signal,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
// OpenAI-compatible (openai-completions, ollama, etc.)
|
|
210
|
+
const endpoint = llmUrl.endsWith("/chat/completions")
|
|
211
|
+
? llmUrl
|
|
212
|
+
: `${llmUrl}/v1/chat/completions`;
|
|
213
|
+
resp = await fetch(endpoint, {
|
|
214
|
+
method: "POST",
|
|
215
|
+
headers: {
|
|
216
|
+
"Content-Type": "application/json",
|
|
217
|
+
"Authorization": `Bearer ${llmApiKey}`,
|
|
218
|
+
...llmHeaders,
|
|
219
|
+
},
|
|
220
|
+
body: JSON.stringify({
|
|
221
|
+
model: llmModel,
|
|
222
|
+
max_tokens: LLM_MAX_TOKENS,
|
|
223
|
+
temperature: 0,
|
|
224
|
+
messages: [
|
|
225
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
226
|
+
{ role: "user", content: userPrompt },
|
|
227
|
+
],
|
|
228
|
+
}),
|
|
229
|
+
signal: controller.signal,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
if (!resp.ok)
|
|
233
|
+
throw new Error(`LLM HTTP ${resp.status}`);
|
|
234
|
+
const data = (await resp.json());
|
|
235
|
+
// Extract text from either Anthropic or OpenAI response format
|
|
236
|
+
const text = data.content?.[0]?.text
|
|
237
|
+
?? data.choices?.[0]?.message?.content
|
|
238
|
+
?? "";
|
|
239
|
+
const jsonMatch = text.match(/\{[\s\S]*?\}/);
|
|
240
|
+
if (!jsonMatch)
|
|
241
|
+
throw new Error("No JSON in LLM response");
|
|
242
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
243
|
+
return { confirmed: !!parsed.confirmed, reason: parsed.reason ?? "" };
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
clearTimeout(timer);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// ── Prompt Builder ─────────────────────────────────────────────────
|
|
250
|
+
function buildPrompt(toolName, params, context) {
|
|
251
|
+
const detail = toolName === "exec"
|
|
252
|
+
? `Command: ${params.command ?? "(empty)"}`
|
|
253
|
+
: `File path: ${params.file_path ?? params.path ?? "(empty)"}`;
|
|
254
|
+
return `Flagged tool call:\n- Tool: ${toolName}\n- ${detail}\n\nRecent user messages:\n${context}`;
|
|
255
|
+
}
|
|
256
|
+
// ── Public API ─────────────────────────────────────────────────────
|
|
257
|
+
export async function singleVote(toolName, params, sessionKey) {
|
|
258
|
+
const context = readRecentContext(sessionKey);
|
|
259
|
+
const prompt = buildPrompt(toolName, params, context);
|
|
260
|
+
try {
|
|
261
|
+
return await callLLM(prompt);
|
|
262
|
+
}
|
|
263
|
+
catch (e) {
|
|
264
|
+
return { confirmed: false, reason: `LLM unavailable: ${e.message}` };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
export async function multiVote(toolName, params, sessionKey, count = 3, threshold = 3) {
|
|
268
|
+
const context = readRecentContext(sessionKey);
|
|
269
|
+
const prompt = buildPrompt(toolName, params, context);
|
|
270
|
+
const promises = Array.from({ length: count }, (_, i) => callLLM(prompt)
|
|
271
|
+
.then((r) => ({ voter: i + 1, confirmed: r.confirmed, reason: r.reason }))
|
|
272
|
+
.catch((e) => ({ voter: i + 1, confirmed: false, reason: `LLM error: ${e.message}` })));
|
|
273
|
+
const votes = await Promise.all(promises);
|
|
274
|
+
const yesCount = votes.filter((v) => v.confirmed).length;
|
|
275
|
+
const confirmed = yesCount >= threshold;
|
|
276
|
+
const reason = confirmed
|
|
277
|
+
? `${yesCount}/${count} voters confirmed user intent`
|
|
278
|
+
: `Only ${yesCount}/${count} confirmed (need ${threshold})`;
|
|
279
|
+
return { confirmed, reason, votes };
|
|
280
|
+
}
|
|
281
|
+
//# sourceMappingURL=llm-voter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"llm-voter.js","sourceRoot":"","sources":["../../src/llm-voter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "openclaw-guardian",
|
|
3
|
+
"name": "OpenClaw Guardian",
|
|
4
|
+
"description": "Security gate plugin — whitelist + LLM judge + rule fallback. 99% of operations pass via whitelist with 0ms latency.",
|
|
5
|
+
"version": "2.0.0",
|
|
6
|
+
"configSchema": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"enabled": { "type": "boolean", "default": true }
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-guardian",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Security gate plugin for OpenClaw — two-tier blacklist (regex + LLM intent verification) prevents dangerous tool executions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist/",
|
|
16
|
+
"openclaw.plugin.json",
|
|
17
|
+
"default-policies.json",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc",
|
|
23
|
+
"prepublishOnly": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"openclaw",
|
|
27
|
+
"openclaw-plugin",
|
|
28
|
+
"security",
|
|
29
|
+
"safety",
|
|
30
|
+
"guardian",
|
|
31
|
+
"blacklist",
|
|
32
|
+
"llm",
|
|
33
|
+
"tool-safety"
|
|
34
|
+
],
|
|
35
|
+
"author": "fatcatMaoFei",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/fatcatMaoFei/openclaw-guardian.git"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/fatcatMaoFei/openclaw-guardian/issues"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/fatcatMaoFei/openclaw-guardian#readme",
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"openclaw": ">=2026.1.26"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"typescript": "^5.9.0"
|
|
50
|
+
}
|
|
51
|
+
}
|