clawmoat 0.8.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/.dockerignore +9 -0
  2. package/CHANGELOG.md +18 -0
  3. package/DEMO.md +87 -0
  4. package/Dockerfile +5 -18
  5. package/README.md +232 -8
  6. package/THREAT_MODEL.md +129 -0
  7. package/agent/README.md +131 -0
  8. package/agent/index.js +471 -0
  9. package/agent/install-service.sh +94 -0
  10. package/agent/openclaw-hook.js +453 -0
  11. package/agent/provider-setup.js +649 -0
  12. package/agent/setup.js +274 -0
  13. package/assets/BADGE-USAGE.md +20 -0
  14. package/assets/clawmoat-badge.svg +21 -0
  15. package/bin/clawmoat.js +468 -111
  16. package/docs/affiliates/dashboard.html +124 -0
  17. package/docs/affiliates/index.html +236 -0
  18. package/docs/agent-install.html +183 -0
  19. package/docs/ai-agent-security-scanner.html +10 -6
  20. package/docs/badge/index.html +149 -0
  21. package/docs/badge/scanning.svg +23 -0
  22. package/docs/blog/386-malicious-skills.html +11 -4
  23. package/docs/blog/40000-exposed-openclaw-instances.html +11 -4
  24. package/docs/blog/agent-trust-protocol.html +5 -4
  25. package/docs/blog/ai-agent-earns-commissions.html +230 -0
  26. package/docs/blog/bugmageddon-agent-firewall.html +174 -0
  27. package/docs/blog/calculator-math.html +180 -0
  28. package/docs/blog/clawmoat-vs-llamafirewall-nemo-guardrails.html +10 -4
  29. package/docs/blog/host-guardian-launch.html +18 -8
  30. package/docs/blog/ibm-experts-agent-runtime-protection.html +15 -6
  31. package/docs/blog/index.html +67 -9
  32. package/docs/blog/langchain-security-tutorial.html +18 -8
  33. package/docs/blog/mcp-30-cves-security-crisis.html +11 -4
  34. package/docs/blog/meta-researcher-rogue-agent.html +201 -0
  35. package/docs/blog/microsoft-openclaw-workstation-security.html +5 -4
  36. package/docs/blog/nist-ai-agent-standards-clawmoat.html +16 -8
  37. package/docs/blog/oasis-websocket-hijack.html +11 -4
  38. package/docs/blog/ollama-openclaw-security.html +10 -4
  39. package/docs/blog/openclaw-enterprise-readiness-claw10.html +5 -4
  40. package/docs/blog/openclaw-security-reckoning-2026.html +11 -4
  41. package/docs/blog/owasp-agentic-ai-top10.html +18 -8
  42. package/docs/blog/securing-ai-agents.html +18 -8
  43. package/docs/blog/supply-chain-agents.html +18 -8
  44. package/docs/business/index.html +11 -16
  45. package/docs/business/install.html +21 -7
  46. package/docs/checklist.html +10 -4
  47. package/docs/compare/index.html +122 -0
  48. package/docs/compare/lakera/index.html +62 -0
  49. package/docs/compare/llm-guard/index.html +49 -0
  50. package/docs/compare/snyk-agent-scan/index.html +63 -0
  51. package/docs/compare.html +10 -6
  52. package/docs/dashboard/index.html +520 -0
  53. package/docs/finance/index.html +9 -6
  54. package/docs/guides/business-deployment.html +770 -0
  55. package/docs/hall-of-fame.html +11 -5
  56. package/docs/index.html +266 -137
  57. package/docs/integrations/langchain.html +14 -6
  58. package/docs/integrations/openai.html +14 -6
  59. package/docs/integrations/openclaw.html +55 -7
  60. package/docs/plans/2026-03-26-threat-intel-api.md +255 -0
  61. package/docs/plans/2026-04-14-bugmageddon-marketing-pack.md +329 -0
  62. package/docs/plans/2026-04-14-clawmoat-v1-bugmageddon.md +248 -0
  63. package/docs/plans/2026-04-14-v1-release-update.md +91 -0
  64. package/docs/plans/2026-04-19-supabase-audit.md +68 -0
  65. package/docs/plans/2026-05-12-sales-push.md +303 -0
  66. package/docs/playground/index.html +893 -0
  67. package/docs/playground.html +4 -7
  68. package/docs/rfcs/defense-in-depth.md +467 -0
  69. package/docs/scan/index.html +156 -12
  70. package/docs/services/case-study.html +255 -0
  71. package/docs/services/downloads/install-openclaw.bat +45 -0
  72. package/docs/services/downloads/install-openclaw.command +38 -0
  73. package/docs/services/downloads/install-openclaw.sh +38 -0
  74. package/docs/services/get-started.html +165 -0
  75. package/docs/services/index.html +598 -0
  76. package/docs/services/multi-agent-security.html +284 -0
  77. package/docs/services/one-pager.html +99 -0
  78. package/docs/services/pitch-deck.html +229 -0
  79. package/docs/services/roi-calculator.html +258 -0
  80. package/docs/sitemap.xml +62 -2
  81. package/docs/support/index.html +12 -1
  82. package/docs/templates/customer-service/HEARTBEAT.md +61 -0
  83. package/docs/templates/customer-service/MEMORY.md +89 -0
  84. package/docs/templates/customer-service/SOUL.md +41 -0
  85. package/docs/templates/customer-service/USER.md +56 -0
  86. package/docs/templates/executive/HEARTBEAT.md +86 -0
  87. package/docs/templates/executive/MEMORY.md +92 -0
  88. package/docs/templates/executive/SOUL.md +44 -0
  89. package/docs/templates/executive/USER.md +62 -0
  90. package/docs/templates/finance/HEARTBEAT.md +58 -0
  91. package/docs/templates/finance/MEMORY.md +87 -0
  92. package/docs/templates/finance/SOUL.md +38 -0
  93. package/docs/templates/finance/USER.md +53 -0
  94. package/docs/templates/index.html +115 -0
  95. package/docs/templates/operations/HEARTBEAT.md +63 -0
  96. package/docs/templates/operations/MEMORY.md +68 -0
  97. package/docs/templates/operations/SOUL.md +38 -0
  98. package/docs/templates/operations/USER.md +49 -0
  99. package/docs/templates/sales/HEARTBEAT.md +55 -0
  100. package/docs/templates/sales/MEMORY.md +89 -0
  101. package/docs/templates/sales/SOUL.md +34 -0
  102. package/docs/templates/sales/USER.md +54 -0
  103. package/eslint.config.js +32 -0
  104. package/evals/README.md +29 -0
  105. package/evals/cases.json +390 -0
  106. package/evals/results.md +68 -0
  107. package/evals/run.js +180 -0
  108. package/examples/demo-attack/demo.js +186 -0
  109. package/examples/python-quickstart/README.md +54 -0
  110. package/examples/python-quickstart/clawmoat_client.py +167 -0
  111. package/examples/video-demo/README.md +14 -0
  112. package/examples/video-demo/scene-a-normal.js +29 -0
  113. package/examples/video-demo/scene-b-attack-arrives.js +31 -0
  114. package/examples/video-demo/scene-c-hijack.js +44 -0
  115. package/examples/video-demo/scene-d-clawmoat.js +46 -0
  116. package/integrations/crewai/README.md +32 -0
  117. package/integrations/crewai/clawmoat_crewai/__init__.py +17 -0
  118. package/integrations/crewai/clawmoat_crewai/guard.py +103 -0
  119. package/integrations/crewai/pyproject.toml +21 -0
  120. package/integrations/langchain/README.md +91 -0
  121. package/integrations/langchain/clawmoat_langchain/__init__.py +17 -0
  122. package/integrations/langchain/clawmoat_langchain/callback.py +489 -0
  123. package/integrations/langchain/pyproject.toml +32 -0
  124. package/integrations/litellm/README.md +324 -0
  125. package/integrations/litellm/clawmoat_litellm/__init__.py +21 -0
  126. package/integrations/litellm/clawmoat_litellm/callback.py +329 -0
  127. package/integrations/litellm/clawmoat_litellm/proxy_middleware.py +224 -0
  128. package/integrations/litellm/pyproject.toml +74 -0
  129. package/integrations/openai-agents/README.md +392 -0
  130. package/integrations/openai-agents/clawmoat_openai_agents/__init__.py +20 -0
  131. package/integrations/openai-agents/clawmoat_openai_agents/guardrail.py +431 -0
  132. package/integrations/openai-agents/clawmoat_openai_agents/middleware.py +311 -0
  133. package/integrations/openai-agents/pyproject.toml +76 -0
  134. package/package.json +6 -5
  135. package/plugins/openclaw-adapter/PHASE1.md +439 -0
  136. package/plugins/openclaw-adapter/README.md +103 -0
  137. package/plugins/openclaw-adapter/SPEC.md +1644 -0
  138. package/plugins/openclaw-adapter/package.json +31 -0
  139. package/plugins/openclaw-adapter/src/index.test.ts +226 -0
  140. package/plugins/openclaw-adapter/src/index.ts +140 -0
  141. package/plugins/openclaw-adapter/tsconfig.json +14 -0
  142. package/server/data/threats.json +290 -0
  143. package/server/index.js +142 -7
  144. package/src/adapters/express.js +161 -0
  145. package/src/adapters/index.js +92 -0
  146. package/src/adapters/langchain.js +185 -0
  147. package/src/approval/index.js +456 -0
  148. package/src/ban-scanner.js +200 -0
  149. package/src/boundary-scanner.js +296 -0
  150. package/src/ci-scanner.js +279 -0
  151. package/src/code-scanner.js +245 -0
  152. package/src/enforce.js +166 -0
  153. package/src/formatters/json.js +80 -0
  154. package/src/formatters/sarif.js +388 -0
  155. package/src/guardian/alerts.js +34 -3
  156. package/src/guardian/index.js +41 -2
  157. package/src/index.js +102 -0
  158. package/src/integrations/agentmesh.js +501 -0
  159. package/src/language-detector.js +201 -0
  160. package/src/mcp-scanner.js +253 -0
  161. package/src/multimodal/index.js +579 -0
  162. package/src/obfuscation-scanner.js +457 -0
  163. package/src/policy-engine.js +402 -0
  164. package/src/scanners/dependency-attacks.js +128 -0
  165. package/src/scanners/prompt-injection.js +18 -0
  166. package/src/scanners/supply-chain.js +14 -0
  167. package/src/templates/default-config.yml +90 -0
  168. package/src/vuln-ops/exploitability.js +46 -0
  169. package/src/watch/live-monitor.js +720 -0
  170. package/clawmoat-0.8.0.tgz +0 -0
  171. package/server/index.js.patch +0 -1
@@ -0,0 +1,456 @@
1
+ /**
2
+ * Interactive Approval Workflow with Pluggable Notification Channels
3
+ * Security primitive for agent governance — agents must ask before dangerous actions
4
+ *
5
+ * @module approval
6
+ * @example
7
+ * const { ApprovalWorkflow, ConsoleChannel } = require('./approval');
8
+ *
9
+ * const workflow = new ApprovalWorkflow({
10
+ * defaultTimeout: 30000,
11
+ * channels: [new ConsoleChannel()]
12
+ * });
13
+ *
14
+ * const approved = await workflow.requestApproval({
15
+ * action: 'delete_file',
16
+ * agent: 'maintenance-bot',
17
+ * reason: 'Cleaning up temporary files in /tmp/agent-cache',
18
+ * timeout: 60000
19
+ * });
20
+ */
21
+
22
+ const { randomUUID } = require('crypto');
23
+ const { createReadStream, createWriteStream, existsSync } = require('fs');
24
+ const { appendFile } = require('fs/promises');
25
+ const { createInterface } = require('readline');
26
+ const http = require('http');
27
+
28
+ /**
29
+ * @typedef {Object} ApprovalRequest
30
+ * @property {string} action - Description of the action requiring approval
31
+ * @property {string} agent - Name/ID of the agent requesting approval
32
+ * @property {string} reason - Human-readable explanation of why this action is needed
33
+ * @property {number} [timeout=30000] - Timeout in ms before auto-approval/denial
34
+ */
35
+
36
+ /**
37
+ * @typedef {Object} ApprovalResponse
38
+ * @property {string} requestId - Unique request identifier
39
+ * @property {boolean} approved - Whether the action was approved
40
+ * @property {string} source - How the decision was made ('user', 'timeout', 'policy')
41
+ * @property {string} [reason] - Optional explanation for the decision
42
+ * @property {number} timestamp - Unix timestamp when decision was made
43
+ */
44
+
45
+ /**
46
+ * Base interface for notification channels
47
+ */
48
+ class NotificationChannel {
49
+ /**
50
+ * Notify about a pending approval request
51
+ * @param {ApprovalRequest & { requestId: string, expiresAt: number }} request
52
+ * @returns {Promise<void>}
53
+ */
54
+ async notify(request) {
55
+ throw new Error('notify() must be implemented');
56
+ }
57
+
58
+ /**
59
+ * Check if a response has been provided for this request
60
+ * @param {string} requestId
61
+ * @returns {Promise<{ approved: boolean, reason?: string } | null>}
62
+ */
63
+ async checkResponse(requestId) {
64
+ throw new Error('checkResponse() must be implemented');
65
+ }
66
+
67
+ /**
68
+ * Cleanup any resources for this request
69
+ * @param {string} requestId
70
+ * @returns {Promise<void>}
71
+ */
72
+ async cleanup(requestId) {
73
+ // Default: no-op
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Console channel - prompts via stdin/stdout
79
+ */
80
+ class ConsoleChannel extends NotificationChannel {
81
+ constructor() {
82
+ super();
83
+ this.pendingRequests = new Map();
84
+ }
85
+
86
+ async notify(request) {
87
+ console.log('\n🚨 APPROVAL REQUIRED 🚨');
88
+ console.log(`Agent: ${request.agent}`);
89
+ console.log(`Action: ${request.action}`);
90
+ console.log(`Reason: ${request.reason}`);
91
+ console.log(`Expires: ${new Date(request.expiresAt).toISOString()}`);
92
+ console.log(`Respond with: approve ${request.requestId} OR deny ${request.requestId} [reason]`);
93
+ console.log('');
94
+
95
+ // Store for response checking
96
+ this.pendingRequests.set(request.requestId, {
97
+ active: true,
98
+ response: null
99
+ });
100
+
101
+ // Start listening for stdin if not already
102
+ if (!this._stdinListener) {
103
+ this._startStdinListener();
104
+ }
105
+ }
106
+
107
+ async checkResponse(requestId) {
108
+ const pending = this.pendingRequests.get(requestId);
109
+ if (!pending || !pending.response) return null;
110
+
111
+ return pending.response;
112
+ }
113
+
114
+ async cleanup(requestId) {
115
+ this.pendingRequests.delete(requestId);
116
+
117
+ if (this.pendingRequests.size === 0 && this._stdinListener) {
118
+ this._stdinListener.close();
119
+ this._stdinListener = null;
120
+ }
121
+ }
122
+
123
+ _startStdinListener() {
124
+ this._stdinListener = createInterface({
125
+ input: process.stdin,
126
+ output: process.stdout
127
+ });
128
+
129
+ this._stdinListener.on('line', (line) => {
130
+ const match = line.trim().match(/^(approve|deny)\s+([a-f0-9-]+)(?:\s+(.+))?$/i);
131
+ if (!match) return;
132
+
133
+ const [, action, requestId, reason] = match;
134
+ const pending = this.pendingRequests.get(requestId);
135
+
136
+ if (pending && pending.active) {
137
+ pending.response = {
138
+ approved: action.toLowerCase() === 'approve',
139
+ reason: reason || undefined
140
+ };
141
+ pending.active = false;
142
+
143
+ console.log(`✅ Response recorded: ${action.toUpperCase()} ${requestId}`);
144
+ }
145
+ });
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Webhook channel - sends HTTP POST and polls for response
151
+ */
152
+ class WebhookChannel extends NotificationChannel {
153
+ constructor(options = {}) {
154
+ super();
155
+ this.webhookUrl = options.webhookUrl;
156
+ this.responseUrl = options.responseUrl; // URL to poll for responses
157
+ this.secret = options.secret; // Optional webhook signature secret
158
+
159
+ if (!this.webhookUrl) {
160
+ throw new Error('WebhookChannel requires webhookUrl option');
161
+ }
162
+ }
163
+
164
+ async notify(request) {
165
+ const payload = JSON.stringify({
166
+ type: 'approval_request',
167
+ requestId: request.requestId,
168
+ action: request.action,
169
+ agent: request.agent,
170
+ reason: request.reason,
171
+ expiresAt: request.expiresAt,
172
+ timestamp: Date.now()
173
+ });
174
+
175
+ const url = new URL(this.webhookUrl);
176
+ const options = {
177
+ hostname: url.hostname,
178
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
179
+ path: url.pathname + url.search,
180
+ method: 'POST',
181
+ headers: {
182
+ 'Content-Type': 'application/json',
183
+ 'Content-Length': Buffer.byteLength(payload),
184
+ 'User-Agent': 'ClawMoat-Approval/1.0'
185
+ }
186
+ };
187
+
188
+ // Add signature if secret provided
189
+ if (this.secret) {
190
+ const crypto = require('crypto');
191
+ const signature = crypto.createHmac('sha256', this.secret).update(payload).digest('hex');
192
+ options.headers['X-ClawMoat-Signature'] = `sha256=${signature}`;
193
+ }
194
+
195
+ return new Promise((resolve, reject) => {
196
+ const req = http.request(options, (res) => {
197
+ if (res.statusCode >= 200 && res.statusCode < 300) {
198
+ resolve();
199
+ } else {
200
+ reject(new Error(`Webhook failed: HTTP ${res.statusCode}`));
201
+ }
202
+ });
203
+
204
+ req.on('error', reject);
205
+ req.write(payload);
206
+ req.end();
207
+ });
208
+ }
209
+
210
+ async checkResponse(requestId) {
211
+ if (!this.responseUrl) return null;
212
+
213
+ const url = new URL(this.responseUrl);
214
+ url.searchParams.set('requestId', requestId);
215
+
216
+ return new Promise((resolve) => {
217
+ const options = {
218
+ hostname: url.hostname,
219
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
220
+ path: url.pathname + url.search,
221
+ method: 'GET',
222
+ headers: { 'User-Agent': 'ClawMoat-Approval/1.0' }
223
+ };
224
+
225
+ const req = http.request(options, (res) => {
226
+ if (res.statusCode !== 200) {
227
+ resolve(null);
228
+ return;
229
+ }
230
+
231
+ let data = '';
232
+ res.on('data', chunk => data += chunk);
233
+ res.on('end', () => {
234
+ try {
235
+ const response = JSON.parse(data);
236
+ if (response.requestId === requestId && response.decision) {
237
+ resolve({
238
+ approved: response.decision === 'approve',
239
+ reason: response.reason
240
+ });
241
+ } else {
242
+ resolve(null);
243
+ }
244
+ } catch {
245
+ resolve(null);
246
+ }
247
+ });
248
+ });
249
+
250
+ req.on('error', () => resolve(null));
251
+ req.setTimeout(5000, () => {
252
+ req.destroy();
253
+ resolve(null);
254
+ });
255
+ req.end();
256
+ });
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Callback channel - uses provided functions for notification and response checking
262
+ */
263
+ class CallbackChannel extends NotificationChannel {
264
+ constructor(options = {}) {
265
+ super();
266
+ this.notifyFn = options.notifyFn;
267
+ this.checkResponseFn = options.checkResponseFn;
268
+
269
+ if (typeof this.notifyFn !== 'function') {
270
+ throw new Error('CallbackChannel requires notifyFn function');
271
+ }
272
+ if (typeof this.checkResponseFn !== 'function') {
273
+ throw new Error('CallbackChannel requires checkResponseFn function');
274
+ }
275
+ }
276
+
277
+ async notify(request) {
278
+ return this.notifyFn(request);
279
+ }
280
+
281
+ async checkResponse(requestId) {
282
+ return this.checkResponseFn(requestId);
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Main approval workflow coordinator
288
+ */
289
+ class ApprovalWorkflow {
290
+ constructor(options = {}) {
291
+ this.defaultTimeout = options.defaultTimeout || 30000;
292
+ this.defaultAction = options.defaultAction || 'deny'; // 'approve' | 'deny'
293
+ this.channels = Array.isArray(options.channels) ? options.channels : [new ConsoleChannel()];
294
+ this.auditLog = options.auditLog || null; // Path to audit log file
295
+ }
296
+
297
+ /**
298
+ * Request approval for an action
299
+ * @param {ApprovalRequest} request
300
+ * @returns {Promise<ApprovalResponse>}
301
+ */
302
+ async requestApproval(request) {
303
+ const requestId = randomUUID();
304
+ const timeout = request.timeout || this.defaultTimeout;
305
+ const expiresAt = Date.now() + timeout;
306
+
307
+ const fullRequest = {
308
+ ...request,
309
+ requestId,
310
+ expiresAt
311
+ };
312
+
313
+ // Log the request
314
+ await this._auditLog({
315
+ type: 'approval_request',
316
+ requestId,
317
+ action: request.action,
318
+ agent: request.agent,
319
+ reason: request.reason,
320
+ timeout,
321
+ timestamp: Date.now()
322
+ });
323
+
324
+ // Notify all channels
325
+ const notificationPromises = this.channels.map(channel =>
326
+ channel.notify(fullRequest).catch(err => {
327
+ console.error(`Channel notification failed: ${err.message}`);
328
+ })
329
+ );
330
+
331
+ await Promise.allSettled(notificationPromises);
332
+
333
+ // Poll for responses with timeout
334
+ return new Promise((resolve) => {
335
+ const pollInterval = Math.min(1000, timeout / 10); // Poll every 1s or 1/10th of timeout
336
+ let timeoutHandle;
337
+ let pollHandle;
338
+
339
+ const checkResponses = async () => {
340
+ for (const channel of this.channels) {
341
+ try {
342
+ const response = await channel.checkResponse(requestId);
343
+ if (response) {
344
+ clearTimeout(timeoutHandle);
345
+ clearInterval(pollHandle);
346
+
347
+ // Cleanup all channels
348
+ await Promise.allSettled(
349
+ this.channels.map(ch => ch.cleanup(requestId))
350
+ );
351
+
352
+ const result = {
353
+ requestId,
354
+ approved: response.approved,
355
+ source: 'user',
356
+ reason: response.reason,
357
+ timestamp: Date.now()
358
+ };
359
+
360
+ await this._auditLog({
361
+ type: 'approval_response',
362
+ ...result
363
+ });
364
+
365
+ resolve(result);
366
+ return;
367
+ }
368
+ } catch (err) {
369
+ // Ignore channel errors during polling
370
+ }
371
+ }
372
+ };
373
+
374
+ // Start polling
375
+ pollHandle = setInterval(checkResponses, pollInterval);
376
+
377
+ // Set timeout
378
+ timeoutHandle = setTimeout(async () => {
379
+ clearInterval(pollHandle);
380
+
381
+ // Cleanup all channels
382
+ await Promise.allSettled(
383
+ this.channels.map(ch => ch.cleanup(requestId))
384
+ );
385
+
386
+ const result = {
387
+ requestId,
388
+ approved: this.defaultAction === 'approve',
389
+ source: 'timeout',
390
+ reason: `No response within ${timeout}ms, defaulting to ${this.defaultAction}`,
391
+ timestamp: Date.now()
392
+ };
393
+
394
+ await this._auditLog({
395
+ type: 'approval_timeout',
396
+ ...result
397
+ });
398
+
399
+ resolve(result);
400
+ }, timeout);
401
+ });
402
+ }
403
+
404
+ /**
405
+ * Get audit log entries
406
+ * @param {Object} [filter] - Optional filter criteria
407
+ * @returns {Promise<Object[]>}
408
+ */
409
+ async getAuditLog(filter = {}) {
410
+ if (!this.auditLog || !existsSync(this.auditLog)) {
411
+ return [];
412
+ }
413
+
414
+ const entries = [];
415
+ const fileStream = createReadStream(this.auditLog);
416
+ const rl = createInterface({ input: fileStream });
417
+
418
+ for await (const line of rl) {
419
+ try {
420
+ const entry = JSON.parse(line);
421
+
422
+ // Apply filters
423
+ if (filter.requestId && entry.requestId !== filter.requestId) continue;
424
+ if (filter.agent && entry.agent !== filter.agent) continue;
425
+ if (filter.type && entry.type !== filter.type) continue;
426
+ if (filter.since && entry.timestamp < filter.since) continue;
427
+ if (filter.until && entry.timestamp > filter.until) continue;
428
+
429
+ entries.push(entry);
430
+ } catch {
431
+ // Skip malformed lines
432
+ }
433
+ }
434
+
435
+ return entries;
436
+ }
437
+
438
+ async _auditLog(entry) {
439
+ if (!this.auditLog) return;
440
+
441
+ const logLine = JSON.stringify(entry) + '\n';
442
+ try {
443
+ await appendFile(this.auditLog, logLine);
444
+ } catch (err) {
445
+ console.error(`Audit log write failed: ${err.message}`);
446
+ }
447
+ }
448
+ }
449
+
450
+ module.exports = {
451
+ ApprovalWorkflow,
452
+ NotificationChannel,
453
+ ConsoleChannel,
454
+ WebhookChannel,
455
+ CallbackChannel
456
+ };
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Ban Topics / Substrings / Allow-Deny Lists
3
+ *
4
+ * Stolen from LLM Guard. Enterprises love obvious controls.
5
+ * Configure banned topics, required keywords, regex patterns, and deny/allow lists.
6
+ *
7
+ * @module ban-scanner
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ /**
13
+ * Create a ban scanner with configurable rules
14
+ * @param {Object} config - Ban configuration
15
+ * @param {string[]} [config.bannedSubstrings=[]] - Exact substrings to block (case-insensitive)
16
+ * @param {RegExp[]|string[]} [config.bannedPatterns=[]] - Regex patterns to block
17
+ * @param {string[]} [config.bannedTopics=[]] - Topic keywords to block
18
+ * @param {string[]} [config.allowedTopics=[]] - If set, ONLY these topics are allowed
19
+ * @param {string[]} [config.requiredSubstrings=[]] - At least one must be present (for output validation)
20
+ * @param {Object} [config.customRules=[]] - Array of {name, test: (text) => bool, severity, message}
21
+ * @returns {Object} Scanner instance
22
+ */
23
+ function createBanScanner(config = {}) {
24
+ const {
25
+ bannedSubstrings = [],
26
+ bannedPatterns = [],
27
+ bannedTopics = [],
28
+ allowedTopics = [],
29
+ requiredSubstrings = [],
30
+ customRules = [],
31
+ } = config;
32
+
33
+ // Pre-compile patterns
34
+ const compiledPatterns = bannedPatterns.map(p =>
35
+ p instanceof RegExp ? p : new RegExp(p, 'gi')
36
+ );
37
+
38
+ /**
39
+ * Scan text against ban rules
40
+ * @param {string} text - Text to scan
41
+ * @returns {Object} { safe, findings }
42
+ */
43
+ function scan(text) {
44
+ const findings = [];
45
+ const lower = text.toLowerCase();
46
+
47
+ // Check banned substrings
48
+ for (const sub of bannedSubstrings) {
49
+ if (lower.includes(sub.toLowerCase())) {
50
+ findings.push({
51
+ type: 'banned_content',
52
+ subtype: 'banned_substring',
53
+ severity: 'high',
54
+ confidence: 1.0,
55
+ evidence: `Banned substring found: "${sub}"`,
56
+ matched: sub,
57
+ recommended_action: 'block',
58
+ });
59
+ }
60
+ }
61
+
62
+ // Check banned patterns
63
+ for (const pattern of compiledPatterns) {
64
+ pattern.lastIndex = 0; // Reset regex state
65
+ const match = pattern.exec(text);
66
+ if (match) {
67
+ findings.push({
68
+ type: 'banned_content',
69
+ subtype: 'banned_pattern',
70
+ severity: 'high',
71
+ confidence: 0.95,
72
+ evidence: `Banned pattern matched: "${match[0].substring(0, 60)}"`,
73
+ matched: match[0].substring(0, 100),
74
+ recommended_action: 'block',
75
+ });
76
+ }
77
+ }
78
+
79
+ // Check banned topics (keyword-based)
80
+ for (const topic of bannedTopics) {
81
+ const topicWords = topic.toLowerCase().split(/\s+/);
82
+ const allPresent = topicWords.every(w => lower.includes(w));
83
+ if (allPresent) {
84
+ findings.push({
85
+ type: 'banned_content',
86
+ subtype: 'banned_topic',
87
+ severity: 'medium',
88
+ confidence: 0.7,
89
+ evidence: `Banned topic detected: "${topic}"`,
90
+ topic,
91
+ recommended_action: 'block',
92
+ });
93
+ }
94
+ }
95
+
96
+ // Check allowed topics (if set, text MUST match at least one)
97
+ if (allowedTopics.length > 0) {
98
+ const matchesAllowed = allowedTopics.some(topic => {
99
+ const topicWords = topic.toLowerCase().split(/\s+/);
100
+ return topicWords.some(w => lower.includes(w));
101
+ });
102
+ if (!matchesAllowed) {
103
+ findings.push({
104
+ type: 'banned_content',
105
+ subtype: 'off_topic',
106
+ severity: 'medium',
107
+ confidence: 0.6,
108
+ evidence: `Text does not match any allowed topics: ${allowedTopics.join(', ')}`,
109
+ recommended_action: 'block',
110
+ });
111
+ }
112
+ }
113
+
114
+ // Check required substrings (output validation)
115
+ if (requiredSubstrings.length > 0) {
116
+ const hasRequired = requiredSubstrings.some(s => lower.includes(s.toLowerCase()));
117
+ if (!hasRequired) {
118
+ findings.push({
119
+ type: 'banned_content',
120
+ subtype: 'missing_required',
121
+ severity: 'low',
122
+ confidence: 0.8,
123
+ evidence: `Missing required content. Expected one of: ${requiredSubstrings.join(', ')}`,
124
+ recommended_action: 'warn',
125
+ });
126
+ }
127
+ }
128
+
129
+ // Custom rules
130
+ for (const rule of customRules) {
131
+ try {
132
+ if (rule.test(text)) {
133
+ findings.push({
134
+ type: 'banned_content',
135
+ subtype: 'custom_rule',
136
+ severity: rule.severity || 'medium',
137
+ confidence: 0.9,
138
+ evidence: rule.message || `Custom rule "${rule.name}" triggered`,
139
+ rule: rule.name,
140
+ recommended_action: rule.action || 'block',
141
+ });
142
+ }
143
+ } catch (_) { /* skip broken rules */ }
144
+ }
145
+
146
+ return {
147
+ safe: findings.length === 0,
148
+ findings,
149
+ };
150
+ }
151
+
152
+ return { scan };
153
+ }
154
+
155
+ // Pre-built rulesets
156
+ const PRESETS = {
157
+ // Block competitor mentions (for support bots)
158
+ noCompetitors: (competitors) => ({
159
+ bannedSubstrings: competitors,
160
+ }),
161
+
162
+ // Block personal information requests
163
+ noPIIRequests: () => ({
164
+ bannedPatterns: [
165
+ /what.{0,20}(social security|ssn|credit card|bank account)/i,
166
+ /tell me your.{0,20}(password|address|phone|email)/i,
167
+ /give me.{0,20}(credentials|access|token|key)/i,
168
+ ],
169
+ }),
170
+
171
+ // Block harmful content
172
+ noHarmful: () => ({
173
+ bannedTopics: [
174
+ 'how to hack', 'how to exploit', 'how to attack',
175
+ 'make a bomb', 'make a weapon', 'create malware',
176
+ 'bypass security', 'disable firewall', 'crack password',
177
+ ],
178
+ }),
179
+
180
+ // Coding agent safety
181
+ codingAgent: () => ({
182
+ bannedPatterns: [
183
+ /rm\s+-rf\s+[\/~]/,
184
+ /curl.*\|\s*bash/,
185
+ /wget.*\|\s*sh/,
186
+ /chmod\s+777/,
187
+ /:()\s*{.*}/,
188
+ ],
189
+ bannedSubstrings: [
190
+ '/etc/shadow',
191
+ 'DROP TABLE',
192
+ 'format c:',
193
+ ],
194
+ }),
195
+ };
196
+
197
+ module.exports = {
198
+ createBanScanner,
199
+ PRESETS,
200
+ };