clawmoat 0.7.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +9 -0
- package/CHANGELOG.md +18 -0
- package/CONTRIBUTING.md +4 -2
- package/DEMO.md +87 -0
- package/Dockerfile +5 -18
- package/README.md +294 -8
- package/SECURITY.md +58 -10
- 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 +262 -0
- package/docs/blog/40000-exposed-openclaw-instances.html +201 -0
- package/docs/blog/agent-trust-protocol.html +198 -0
- 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 +229 -0
- package/docs/blog/host-guardian-launch.html +18 -8
- package/docs/blog/ibm-experts-agent-runtime-protection.html +247 -0
- package/docs/blog/index.html +211 -9
- package/docs/blog/langchain-security-tutorial.html +18 -8
- package/docs/blog/mcp-30-cves-security-crisis.html +286 -0
- package/docs/blog/meta-researcher-rogue-agent.html +201 -0
- package/docs/blog/microsoft-openclaw-workstation-security.html +235 -0
- package/docs/blog/nist-ai-agent-standards-clawmoat.html +377 -0
- package/docs/blog/oasis-websocket-hijack.html +212 -0
- package/docs/blog/ollama-openclaw-security.html +160 -0
- package/docs/blog/openclaw-enterprise-readiness-claw10.html +199 -0
- package/docs/blog/openclaw-security-reckoning-2026.html +368 -0
- 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 +525 -0
- package/docs/business/install.html +261 -0
- package/docs/checklist.html +174 -0
- 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 +220 -0
- package/docs/guides/business-deployment.html +770 -0
- package/docs/hall-of-fame.html +174 -0
- package/docs/index.html +447 -154
- package/docs/install.sh +557 -0
- 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/privacy-policy/index.html +122 -0
- package/docs/rfcs/defense-in-depth.md +467 -0
- package/docs/scan/index.html +358 -0
- 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 +192 -2
- package/docs/support/index.html +135 -0
- 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/docs/terms-of-service/index.html +122 -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/basic-usage.js +38 -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 +224 -10
- 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/finance/index.js +585 -0
- package/src/finance/mcp-firewall.js +486 -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/gateway-monitor.js +590 -0
- package/src/guardian/index.js +41 -2
- package/src/index.js +105 -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
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
# Pluggable Sanitizer Interface — Phase 1: Interface & Loading
|
|
2
|
+
|
|
3
|
+
## CC Instruction Set
|
|
4
|
+
|
|
5
|
+
Implement Phase 1 of the pluggable sanitizer interface for the OpenClaw
|
|
6
|
+
session memory sanitization system. This phase defines the TypeScript
|
|
7
|
+
interfaces, config schema, module loading, and startup validation. No
|
|
8
|
+
pipeline wiring yet — that's Phase 2+.
|
|
9
|
+
|
|
10
|
+
**Reference spec:** `pluggable-sanitizer-interface-spec-v1_2_1.md`
|
|
11
|
+
|
|
12
|
+
Do not change any existing sanitization behavior. No pipeline integration,
|
|
13
|
+
no audit events, no frequency scoring changes. This phase only adds the
|
|
14
|
+
foundational types and the loader that validates and initializes plugins
|
|
15
|
+
at startup.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Branch
|
|
20
|
+
|
|
21
|
+
Create a new branch from the tip of `feature/sanitization-hardening`
|
|
22
|
+
(or `main` if that PR has merged):
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
feature/pluggable-sanitizer-interface
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Step 1 — TypeScript Interfaces
|
|
31
|
+
|
|
32
|
+
Create `src/memory/session-sanitization/plugin-interface.ts`.
|
|
33
|
+
|
|
34
|
+
Define and export these interfaces exactly as specified:
|
|
35
|
+
|
|
36
|
+
### `SanitizerPlugin`
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
export interface SanitizerPlugin {
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
phase: "pre" | "post";
|
|
43
|
+
ruleIdPrefix: string;
|
|
44
|
+
initialize(config: Record<string, unknown>): Promise<void>;
|
|
45
|
+
shutdown(): Promise<void>;
|
|
46
|
+
inspect(input: PluginInput): Promise<PluginResult>;
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### `PluginInput`
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
export interface PluginInput {
|
|
54
|
+
content: {
|
|
55
|
+
source: "transcript" | "mcp";
|
|
56
|
+
raw: unknown;
|
|
57
|
+
query?: { server: string; tool: string; params: unknown };
|
|
58
|
+
};
|
|
59
|
+
priorResults: {
|
|
60
|
+
syntactic: { pass: boolean; flags: string[]; ruleIds: string[] };
|
|
61
|
+
schema: { pass: boolean; violations: string[]; ruleIds: string[] };
|
|
62
|
+
semantic?: { safe: boolean; flags: string[]; structuredResult: unknown };
|
|
63
|
+
priorPlugins: PluginResultMeta[];
|
|
64
|
+
};
|
|
65
|
+
contextProfile: string;
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### `PluginResult`
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
export interface PluginResult {
|
|
73
|
+
pluginId: string;
|
|
74
|
+
safe: boolean;
|
|
75
|
+
ruleIds: string[];
|
|
76
|
+
flags: string[];
|
|
77
|
+
confidence: number;
|
|
78
|
+
findingConfidence?: Record<string, number>;
|
|
79
|
+
transformed?: unknown;
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### `PluginResultMeta`
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
export interface PluginResultMeta extends PluginResult {
|
|
87
|
+
transformApplied?: boolean;
|
|
88
|
+
errored?: boolean;
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### `PluginDeclaration` (config shape)
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
export interface PluginDeclaration {
|
|
96
|
+
module: string;
|
|
97
|
+
phase: "pre" | "post";
|
|
98
|
+
enabled?: boolean; // default: true
|
|
99
|
+
config?: Record<string, unknown>; // default: {}
|
|
100
|
+
timeoutMs?: number; // default: 1000, min: 100, max: 10000
|
|
101
|
+
allowTransform?: boolean; // default: false
|
|
102
|
+
frequencyWeight?: number; // default: 3
|
|
103
|
+
maxQueueDepth?: number; // default: 10
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface PluginLimits {
|
|
107
|
+
maxTotal?: number; // default: 10
|
|
108
|
+
maxPrePhase?: number; // default: 5
|
|
109
|
+
maxPostPhase?: number; // default: 5
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Checkpoint:** `pnpm tsgo` passes. No runtime behavior changed.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Step 2 — Config Schema
|
|
118
|
+
|
|
119
|
+
Extend the sanitization config namespace to accept plugin declarations.
|
|
120
|
+
Find where `memory.sessions.sanitization` config is defined (likely in
|
|
121
|
+
`src/memory/session-sanitization/config.ts` or a shared config module).
|
|
122
|
+
|
|
123
|
+
Add two new fields:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
plugins?: PluginDeclaration[]; // default: []
|
|
127
|
+
pluginLimits?: PluginLimits; // default: { maxTotal: 10, maxPrePhase: 5, maxPostPhase: 5 }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
These are **sibling** keys — `plugins` is an array, `pluginLimits` is
|
|
131
|
+
a separate object. They must NOT be nested under the same key.
|
|
132
|
+
|
|
133
|
+
Provide defaults so the system behaves identically when no plugins are
|
|
134
|
+
configured (empty array, nothing loads, nothing changes).
|
|
135
|
+
|
|
136
|
+
**Checkpoint:** `pnpm tsgo` passes. Existing config loading still works.
|
|
137
|
+
No plugins configured = no behavior change.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Step 3 — Built-In Prefix Registry
|
|
142
|
+
|
|
143
|
+
Create `src/memory/session-sanitization/plugin-rule-prefixes.ts`.
|
|
144
|
+
|
|
145
|
+
Export a constant set of built-in rule prefixes that plugins must not
|
|
146
|
+
collide with:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
export const BUILTIN_RULE_PREFIXES: ReadonlySet<string> = new Set([
|
|
150
|
+
"INJ-",
|
|
151
|
+
"CRED-",
|
|
152
|
+
"STRUCT-",
|
|
153
|
+
"TYPE-",
|
|
154
|
+
"ENC-",
|
|
155
|
+
"TEMPORAL-",
|
|
156
|
+
"schema.",
|
|
157
|
+
"injection.",
|
|
158
|
+
"credential.",
|
|
159
|
+
"scope-creep.",
|
|
160
|
+
]);
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Export a validation function:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
export function collidesWithBuiltin(prefix: string): boolean {
|
|
167
|
+
for (const builtin of BUILTIN_RULE_PREFIXES) {
|
|
168
|
+
if (prefix.startsWith(builtin) || builtin.startsWith(prefix)) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Checkpoint:** Unit test for `collidesWithBuiltin`:
|
|
177
|
+
- `"INJ-custom"` → true
|
|
178
|
+
- `"schema.extra"` → true
|
|
179
|
+
- `"clawmoat.scanner"` → false
|
|
180
|
+
- `"acme.hipaa"` → false
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Step 4 — Path Validation
|
|
185
|
+
|
|
186
|
+
Create `src/memory/session-sanitization/plugin-path-validation.ts`.
|
|
187
|
+
|
|
188
|
+
Export a function that validates a plugin module path against the config
|
|
189
|
+
directory. The function must be **platform-agnostic** — works on both
|
|
190
|
+
Windows and POSIX:
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
import * as path from "path";
|
|
194
|
+
import * as fs from "fs";
|
|
195
|
+
|
|
196
|
+
export function validatePluginPath(
|
|
197
|
+
modulePath: string,
|
|
198
|
+
configDir: string
|
|
199
|
+
): { valid: true; resolvedPath: string } | { valid: false; reason: string } {
|
|
200
|
+
// 1. Resolve to absolute
|
|
201
|
+
const absolute = path.resolve(configDir, modulePath);
|
|
202
|
+
|
|
203
|
+
// 2. Resolve symlinks
|
|
204
|
+
let realPath: string;
|
|
205
|
+
try {
|
|
206
|
+
realPath = fs.realpathSync(absolute);
|
|
207
|
+
} catch (err) {
|
|
208
|
+
return { valid: false, reason: `Module not found: ${absolute}` };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 3. Containment check — resolved path must start with configDir + separator
|
|
212
|
+
const configDirReal = fs.realpathSync(configDir);
|
|
213
|
+
const boundary = configDirReal + path.sep;
|
|
214
|
+
if (!realPath.startsWith(boundary) && realPath !== configDirReal) {
|
|
215
|
+
return {
|
|
216
|
+
valid: false,
|
|
217
|
+
reason: `Module path escapes config directory: ${realPath} is outside ${configDirReal}`,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { valid: true, resolvedPath: realPath };
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Checkpoint:** Unit tests:
|
|
226
|
+
- Relative path within config dir → valid
|
|
227
|
+
- Path with `..` escaping → invalid
|
|
228
|
+
- Absolute path outside config dir → invalid
|
|
229
|
+
- Non-existent path → invalid with "not found"
|
|
230
|
+
- (If testable on the platform) symlink pointing outside → invalid
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Step 5 — Plugin Loader
|
|
235
|
+
|
|
236
|
+
Create `src/memory/session-sanitization/plugin-loader.ts`.
|
|
237
|
+
|
|
238
|
+
This is the core of Phase 1. The loader validates config, loads modules,
|
|
239
|
+
validates the plugin interface, and calls `initialize()`. It does NOT
|
|
240
|
+
spawn worker threads yet — that's Phase 2. For now, plugins load in-process.
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
export interface LoadedPlugin {
|
|
244
|
+
declaration: PluginDeclaration;
|
|
245
|
+
instance: SanitizerPlugin;
|
|
246
|
+
resolvedPath: string;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export async function loadPlugins(
|
|
250
|
+
declarations: PluginDeclaration[],
|
|
251
|
+
limits: Required<PluginLimits>,
|
|
252
|
+
configDir: string
|
|
253
|
+
): Promise<LoadedPlugin[]> {
|
|
254
|
+
// Implementation steps below
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Validation order (fail-fast at startup):
|
|
259
|
+
|
|
260
|
+
1. **Filter disabled plugins.** Skip entries with `enabled: false` entirely
|
|
261
|
+
— do not load, do not validate path, do not count against limits.
|
|
262
|
+
|
|
263
|
+
2. **Count limits.** Count enabled plugins per phase and total.
|
|
264
|
+
If any limit exceeded → throw with clear message naming the limit and
|
|
265
|
+
the count.
|
|
266
|
+
|
|
267
|
+
3. **Transform uniqueness.** Per phase, at most one plugin may have
|
|
268
|
+
`allowTransform: true`. If violated → throw naming both plugins.
|
|
269
|
+
|
|
270
|
+
4. **Path validation.** For each enabled plugin, call `validatePluginPath`.
|
|
271
|
+
If invalid → throw with the reason.
|
|
272
|
+
|
|
273
|
+
5. **Module loading.** `require()` the resolved path. Apply CJS interop:
|
|
274
|
+
`const factory = mod.default || mod`. If result is not a function →
|
|
275
|
+
throw with "Module does not export a factory function".
|
|
276
|
+
|
|
277
|
+
6. **Factory call.** `const instance = factory()`. Validate the returned
|
|
278
|
+
object has all required `SanitizerPlugin` fields (id, name, phase,
|
|
279
|
+
ruleIdPrefix, initialize, shutdown, inspect) with correct types.
|
|
280
|
+
If invalid → throw naming the missing/wrong field.
|
|
281
|
+
|
|
282
|
+
7. **Phase consistency.** `declaration.phase` must equal `instance.phase`.
|
|
283
|
+
If mismatched → throw naming both values.
|
|
284
|
+
|
|
285
|
+
8. **ruleIdPrefix validation.**
|
|
286
|
+
- `instance.ruleIdPrefix` must equal `instance.id`.
|
|
287
|
+
- Must not collide with built-in prefixes (use `collidesWithBuiltin`).
|
|
288
|
+
- Must not collide with any previously loaded plugin's prefix.
|
|
289
|
+
If violated → throw naming the collision.
|
|
290
|
+
|
|
291
|
+
9. **Initialize.** Call `await instance.initialize(declaration.config ?? {})`.
|
|
292
|
+
If it throws → let the error propagate (startup failure, fail closed).
|
|
293
|
+
|
|
294
|
+
10. **Return.** Collect all `LoadedPlugin` entries.
|
|
295
|
+
|
|
296
|
+
### Shutdown helper:
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
export async function shutdownPlugins(
|
|
300
|
+
plugins: LoadedPlugin[]
|
|
301
|
+
): Promise<void> {
|
|
302
|
+
// Reverse order. Best-effort — log errors but don't throw.
|
|
303
|
+
for (const plugin of [...plugins].reverse()) {
|
|
304
|
+
try {
|
|
305
|
+
await plugin.instance.shutdown();
|
|
306
|
+
} catch (err) {
|
|
307
|
+
log.warn(`Plugin ${plugin.instance.id} shutdown error`, err);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**Checkpoint:** `pnpm tsgo` passes. Write unit tests for the full loader:
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Step 6 — Loader Tests
|
|
318
|
+
|
|
319
|
+
Create `src/memory/session-sanitization/plugin-loader.test.ts`.
|
|
320
|
+
|
|
321
|
+
Write tests using Vitest. You will need to create small fixture plugins
|
|
322
|
+
as test helpers — either inline mock factories or tiny CJS files in a
|
|
323
|
+
test fixtures directory.
|
|
324
|
+
|
|
325
|
+
**Tests to write:**
|
|
326
|
+
|
|
327
|
+
Loading:
|
|
328
|
+
- Empty declarations array → returns empty array, no errors
|
|
329
|
+
- Single valid plugin → loads, initializes, returns LoadedPlugin
|
|
330
|
+
- Plugin with `enabled: false` → not loaded, not counted
|
|
331
|
+
- Plugin loaded via `module.exports = fn` form → works
|
|
332
|
+
- Plugin loaded via `exports.default = fn` form → works
|
|
333
|
+
|
|
334
|
+
Limit enforcement:
|
|
335
|
+
- Exceeding `maxTotal` → startup error naming the limit
|
|
336
|
+
- Exceeding `maxPrePhase` → startup error
|
|
337
|
+
- Exceeding `maxPostPhase` → startup error
|
|
338
|
+
- Disabled plugins do not count against limits
|
|
339
|
+
|
|
340
|
+
Validation:
|
|
341
|
+
- Phase mismatch (config: "pre", plugin: "post") → startup error
|
|
342
|
+
- ruleIdPrefix not equal to id → startup error
|
|
343
|
+
- ruleIdPrefix colliding with built-in prefix → startup error
|
|
344
|
+
- Two plugins with same id → startup error
|
|
345
|
+
- Two plugins with same ruleIdPrefix → startup error
|
|
346
|
+
- Module not found → startup error with path
|
|
347
|
+
- Module exports non-function → startup error
|
|
348
|
+
- Factory returns object missing required fields → startup error
|
|
349
|
+
|
|
350
|
+
Transform:
|
|
351
|
+
- Two plugins in same phase with `allowTransform: true` → startup error
|
|
352
|
+
- Two plugins in different phases with `allowTransform: true` → OK
|
|
353
|
+
|
|
354
|
+
Path validation:
|
|
355
|
+
- Module path escaping config dir → startup error
|
|
356
|
+
- Valid relative path → resolves and loads
|
|
357
|
+
|
|
358
|
+
Initialize:
|
|
359
|
+
- Plugin that throws in initialize → propagates as startup error
|
|
360
|
+
- Plugin initialize receives the config block from declaration
|
|
361
|
+
|
|
362
|
+
Shutdown:
|
|
363
|
+
- Shutdown calls plugins in reverse order
|
|
364
|
+
- Shutdown logs errors but does not throw
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## Step 7 — Wire Loader Into Startup
|
|
369
|
+
|
|
370
|
+
Find where the sanitization subsystem initializes at agent startup. This
|
|
371
|
+
is likely in the same path where the sanitization config is loaded and
|
|
372
|
+
the sub-agent is prepared.
|
|
373
|
+
|
|
374
|
+
Add a call to `loadPlugins()` after config is resolved and validated.
|
|
375
|
+
Store the resulting `LoadedPlugin[]` in the sanitization state so it's
|
|
376
|
+
accessible to the pipeline (Phase 2+).
|
|
377
|
+
|
|
378
|
+
If no plugins are configured (empty array), the loader returns immediately
|
|
379
|
+
with an empty array. Zero overhead, zero behavior change.
|
|
380
|
+
|
|
381
|
+
Add a corresponding `shutdownPlugins()` call on the agent shutdown path.
|
|
382
|
+
|
|
383
|
+
**Do NOT wire plugins into the inspection pipeline yet.** The loaded plugins
|
|
384
|
+
sit in state, initialized and ready, but not invoked on any content. That
|
|
385
|
+
wiring is Phase 2 (worker isolation) and Phase 3 (pre-phase integration).
|
|
386
|
+
|
|
387
|
+
**Checkpoint:** Full test suite passes:
|
|
388
|
+
- `pnpm vitest run src/memory/session-sanitization/plugin-loader.test.ts`
|
|
389
|
+
- `pnpm vitest run src/memory/session-sanitization/plugin-path-validation.test.ts`
|
|
390
|
+
- `pnpm vitest run src/memory/session-sanitization/plugin-rule-prefixes.test.ts`
|
|
391
|
+
- `pnpm tsgo`
|
|
392
|
+
- `pnpm test` (full suite — confirm no regressions)
|
|
393
|
+
- `pnpm format`
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## Commit
|
|
398
|
+
|
|
399
|
+
Single commit on `feature/pluggable-sanitizer-interface`.
|
|
400
|
+
|
|
401
|
+
```
|
|
402
|
+
feat: add pluggable sanitizer interface types, config schema, and plugin loader
|
|
403
|
+
|
|
404
|
+
Phase 1 of pluggable-sanitizer-interface-spec-v1.2.1.
|
|
405
|
+
Defines SanitizerPlugin/PluginInput/PluginResult/PluginResultMeta interfaces,
|
|
406
|
+
config schema (plugins array + pluginLimits), platform-agnostic path validation,
|
|
407
|
+
built-in prefix collision checking, and startup plugin loading with full
|
|
408
|
+
validation chain. Plugins load and initialize but are not wired into the
|
|
409
|
+
inspection pipeline (Phase 2+).
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## What this does NOT include (deferred to later phases)
|
|
415
|
+
|
|
416
|
+
- Worker thread isolation (Phase 2)
|
|
417
|
+
- Pipeline integration — pre-phase or post-phase execution (Phase 3–4)
|
|
418
|
+
- Content transformation support (Phase 5)
|
|
419
|
+
- Audit events (plugin_config_loaded, etc.) (Phase 3–4)
|
|
420
|
+
- Alerting integration (pluginErrorSpike) (Phase 6)
|
|
421
|
+
- Frequency scoring integration (Phase 3)
|
|
422
|
+
- Context profile plugin overrides (Phase 3)
|
|
423
|
+
- Trust tier routing awareness (Phase 3)
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## Self-Review Checklist (run before committing)
|
|
428
|
+
|
|
429
|
+
- [ ] All new files are under `src/memory/session-sanitization/`
|
|
430
|
+
- [ ] Interfaces match the spec exactly (field names, types, optionality)
|
|
431
|
+
- [ ] Config defaults mean zero behavior change when no plugins configured
|
|
432
|
+
- [ ] Path validation uses `path.resolve` + `fs.realpathSync` + trailing `path.sep`
|
|
433
|
+
- [ ] CJS interop uses `mod.default || mod`
|
|
434
|
+
- [ ] Loader fails fast with clear error messages at each validation step
|
|
435
|
+
- [ ] Shutdown is reverse-order and best-effort
|
|
436
|
+
- [ ] No pipeline changes — loaded plugins sit in state, uninvoked
|
|
437
|
+
- [ ] All tests pass, including full `pnpm test`
|
|
438
|
+
- [ ] `pnpm tsgo` clean
|
|
439
|
+
- [ ] `pnpm format` clean
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# @openclaw/plugin-clawmoat
|
|
2
|
+
|
|
3
|
+
OpenClaw sanitizer plugin wrapping [ClawMoat](https://github.com/darfaz/clawmoat)'s
|
|
4
|
+
prompt injection detection, secret scanning, and PII detection.
|
|
5
|
+
|
|
6
|
+
## What it does
|
|
7
|
+
|
|
8
|
+
Runs ClawMoat's `scan()` function as a **pre-phase** sanitizer plugin.
|
|
9
|
+
Every piece of untrusted content (transcript or MCP result) passes through
|
|
10
|
+
ClawMoat's pattern matching and entropy analysis before reaching the
|
|
11
|
+
semantic sub-agent.
|
|
12
|
+
|
|
13
|
+
- Sub-millisecond execution — pure regex/entropy, no model calls, no network
|
|
14
|
+
- 30+ credential patterns, OWASP Agentic AI coverage
|
|
15
|
+
- Deterministic results (confidence always 1.0)
|
|
16
|
+
- Complements OpenClaw's built-in Tier 1 patterns with ClawMoat's own library
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
cd plugins/clawmoat-adapter
|
|
22
|
+
npm install
|
|
23
|
+
npm run build
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
This produces `dist/index.js` — a single-file CJS bundle with ClawMoat inlined.
|
|
27
|
+
|
|
28
|
+
## Configure
|
|
29
|
+
|
|
30
|
+
In your OpenClaw config:
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
memory.sessions.sanitization.plugins:
|
|
34
|
+
- module: "./plugins/clawmoat-adapter/dist/index.js"
|
|
35
|
+
phase: "pre"
|
|
36
|
+
config:
|
|
37
|
+
# ClawMoat policy config (passed through to createPolicy)
|
|
38
|
+
policy:
|
|
39
|
+
secretPatterns: ["AWS_*", "GITHUB_TOKEN"]
|
|
40
|
+
blockedCommands: ["rm -rf", "curl * | sh"]
|
|
41
|
+
# Block threshold — which severity level triggers a block?
|
|
42
|
+
# Default: "low" (any finding blocks)
|
|
43
|
+
# Options: "low" | "medium" | "high" | "critical"
|
|
44
|
+
blockThreshold: "low"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Block threshold
|
|
48
|
+
|
|
49
|
+
By default, any ClawMoat finding (even `low` severity) produces a block.
|
|
50
|
+
This is the safest setting. If you're seeing false positives on low-severity
|
|
51
|
+
findings, you can raise the threshold:
|
|
52
|
+
|
|
53
|
+
| `blockThreshold` | Blocks on | Flags (but passes) |
|
|
54
|
+
| --- | --- | --- |
|
|
55
|
+
| `"low"` (default) | low, medium, high, critical | — |
|
|
56
|
+
| `"medium"` | medium, high, critical | low |
|
|
57
|
+
| `"high"` | high, critical | low, medium |
|
|
58
|
+
| `"critical"` | critical only | low, medium, high |
|
|
59
|
+
|
|
60
|
+
Findings below the threshold still appear in the audit trail as flags and
|
|
61
|
+
contribute to frequency scoring. They're not ignored — they just don't
|
|
62
|
+
independently trigger a block.
|
|
63
|
+
|
|
64
|
+
## How it maps to OpenClaw
|
|
65
|
+
|
|
66
|
+
| ClawMoat concept | OpenClaw mapping |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| `result.blocked` | `PluginResult.safe` (inverted) |
|
|
69
|
+
| `result.threats[].pattern` | `PluginResult.ruleIds` (prefixed with `clawmoat.scanner.`) |
|
|
70
|
+
| `result.threats[].match` | `PluginResult.flags` |
|
|
71
|
+
| `result.threats[].severity` | Used for block threshold decision |
|
|
72
|
+
| `createPolicy()` config | Passed through from plugin `config.policy` |
|
|
73
|
+
|
|
74
|
+
## Overlap with built-in Tier 1
|
|
75
|
+
|
|
76
|
+
ClawMoat's injection and credential patterns overlap with OpenClaw's
|
|
77
|
+
built-in Tier 1 pre-filter (INJ-*, CRED-*). This is intentional —
|
|
78
|
+
defense in depth. ClawMoat maintains its own pattern library which may
|
|
79
|
+
catch things the built-in set misses, and vice versa. Duplicate findings
|
|
80
|
+
are deduplicated by ruleId in the final merge.
|
|
81
|
+
|
|
82
|
+
## Session-Aware Pipeline (Drawbridge)
|
|
83
|
+
|
|
84
|
+
This adapter provides single-scan integration. For production deployments that need session tracking, frequency-based escalation, content redaction, and compliance audit trails, see [clawmoat-drawbridge](https://github.com/ziomancer/clawmoat-drawbridge) — a session-aware pipeline that wraps ClawMoat with:
|
|
85
|
+
|
|
86
|
+
- Exponential-decay frequency tracking with 3 escalation tiers
|
|
87
|
+
- 16-rule syntactic pre-filter
|
|
88
|
+
- Context profiles (general, medical, financial, code, MCP)
|
|
89
|
+
- Structured audit events with typed payloads
|
|
90
|
+
- Cross-session alert rules with aggregation
|
|
91
|
+
- Content sanitization/redaction
|
|
92
|
+
|
|
93
|
+
The adapter and Drawbridge are complementary: the adapter handles OpenClaw plugin interface compliance, Drawbridge handles session-level orchestration.
|
|
94
|
+
|
|
95
|
+
## Development
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npm run typecheck # Type check without emitting
|
|
99
|
+
npm run build # Bundle to dist/index.js
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The build uses esbuild to produce a single CJS file with clawmoat inlined,
|
|
103
|
+
following the plugin spec's recommended bundling approach.
|