@contextfort-ai/openclaw-secure 0.1.1 → 0.1.3

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.
@@ -31,7 +31,7 @@ if (args[0] === 'set-key') {
31
31
  const key = args[1];
32
32
  if (!key) {
33
33
  console.error('Usage: openclaw-secure set-key <your-api-key>');
34
- console.error('Get your key at https://contextfort.ai');
34
+ console.error('Get your key at https://contextfort.ai/login');
35
35
  process.exit(1);
36
36
  }
37
37
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
@@ -45,7 +45,7 @@ if (args[0] === 'enable') {
45
45
  let hasKey = false;
46
46
  try { hasKey = fs.readFileSync(CONFIG_FILE, 'utf8').trim().length > 0; } catch {}
47
47
  if (!hasKey) {
48
- console.error('No API key found. Get your key at https://contextfort.ai and run:');
48
+ console.error('No API key found. Get your key at https://contextfort.ai/login and run:');
49
49
  console.error(' openclaw-secure set-key <your-key>');
50
50
  process.exit(1);
51
51
  }
@@ -57,6 +57,17 @@ if (args[0] === 'enable') {
57
57
  console.error('openclaw not found. Install it first: npm install -g openclaw');
58
58
  process.exit(1);
59
59
  }
60
+ // Handle --no-skill-deliver flag
61
+ const noSkillDeliver = args.includes('--no-skill-deliver');
62
+ const prefsFile = path.join(CONFIG_DIR, 'preferences.json');
63
+ let prefs = {};
64
+ try { prefs = JSON.parse(fs.readFileSync(prefsFile, 'utf8')); } catch {}
65
+ prefs.skillDeliver = !noSkillDeliver;
66
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
67
+ fs.writeFileSync(prefsFile, JSON.stringify(prefs, null, 2) + '\n', { mode: 0o600 });
68
+ if (noSkillDeliver) {
69
+ console.log('Skill file scanning disabled. Only local checks will run.');
70
+ }
60
71
  try {
61
72
  const original = fs.readlinkSync(openclawLink);
62
73
  fs.writeFileSync(backupLink, original);
@@ -65,6 +76,7 @@ if (args[0] === 'enable') {
65
76
  fs.unlinkSync(openclawLink);
66
77
  fs.symlinkSync(wrapper, openclawLink);
67
78
  console.log('openclaw-secure enabled. `openclaw` is now guarded.');
79
+ console.log('Restart your openclaw gateway for the guard to take effect.');
68
80
  process.exit(0);
69
81
  }
70
82
 
@@ -1,21 +1,90 @@
1
1
  'use strict';
2
2
 
3
- // Only scan commands that fetch Notion pages
4
- const SCAN_PATTERNS = [
3
+ const path = require('path');
4
+
5
+ // Hardcoded fallback patterns (used when server is unreachable)
6
+ const DEFAULT_SCAN_PATTERNS = [
5
7
  'curl -s "https://api.notion.com/v1/pages/',
8
+ 'curl "https://api.notion.com/v1/pages/',
9
+ 'curl -s "https://api.notion.com/v1/blocks/',
10
+ 'curl "https://api.notion.com/v1/blocks/',
6
11
  ];
7
12
 
8
- module.exports = function createPromptInjectionGuard({ httpsRequest, anthropicKey, analytics }) {
13
+ const PATTERNS_CACHE_FILE = '.scan_patterns_cache.json';
14
+
15
+ module.exports = function createPromptInjectionGuard({ httpsRequest, anthropicKey, analytics, readFileSync, apiKey, baseDir }) {
9
16
  const track = analytics ? analytics.track.bind(analytics) : () => {};
10
17
  const flaggedOutput = new Map(); // id → { suspicious, reason, command }
11
18
  const pendingScans = new Set(); // scan ids currently in-flight
12
19
  let scanCounter = 0;
20
+ let scanPatterns = [...DEFAULT_SCAN_PATTERNS];
21
+
22
+ // Load cached patterns from disk
23
+ const cacheFile = baseDir ? path.join(baseDir, 'monitor', PATTERNS_CACHE_FILE) : null;
24
+ if (cacheFile && readFileSync) {
25
+ try {
26
+ const cached = JSON.parse(readFileSync(cacheFile, 'utf8'));
27
+ if (Array.isArray(cached.patterns) && cached.patterns.length > 0) {
28
+ scanPatterns = cached.patterns;
29
+ }
30
+ } catch {}
31
+ }
32
+
33
+ // Fetch patterns from server (non-blocking, updates in background)
34
+ function fetchPatternsFromServer() {
35
+ if (!httpsRequest || !apiKey) return;
36
+
37
+ const options = {
38
+ hostname: 'lschqndjjwtyrlcojvly.supabase.co',
39
+ port: 443,
40
+ path: '/rest/v1/scan_patterns?select=pattern&is_active=eq.true',
41
+ method: 'GET',
42
+ headers: {
43
+ 'apikey': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxzY2hxbmRqand0eXJsY29qdmx5Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzA0NDE3MTEsImV4cCI6MjA4NjAxNzcxMX0.NAC9Tx5a_HswXPC41sDocDPZGuKLgDD-IujX7MSW0I0',
44
+ 'Authorization': `Bearer ${apiKey}`,
45
+ },
46
+ timeout: 10000,
47
+ };
48
+
49
+ try {
50
+ const req = httpsRequest(options, (res) => {
51
+ let body = '';
52
+ res.on('data', (chunk) => { body += chunk; });
53
+ res.on('end', () => {
54
+ if (res.statusCode === 200) {
55
+ try {
56
+ const rows = JSON.parse(body);
57
+ if (Array.isArray(rows) && rows.length > 0) {
58
+ const patterns = rows.map(r => r.pattern).filter(Boolean);
59
+ if (patterns.length > 0) {
60
+ scanPatterns = patterns;
61
+ // Cache to disk
62
+ if (cacheFile) {
63
+ try {
64
+ const fs = require('fs');
65
+ fs.writeFileSync(cacheFile, JSON.stringify({ patterns, updated: new Date().toISOString() }));
66
+ } catch {}
67
+ }
68
+ }
69
+ }
70
+ // If server returns empty, keep current patterns (fallback)
71
+ } catch {}
72
+ }
73
+ // Non-200: fail-open, keep current patterns
74
+ });
75
+ });
76
+
77
+ req.on('error', () => {});
78
+ req.on('timeout', () => { req.destroy(); });
79
+ req.end();
80
+ } catch {}
81
+ }
13
82
 
14
83
  function shouldScanCommand(cmd) {
15
84
  if (!cmd || typeof cmd !== 'string') return false;
16
85
  if (!anthropicKey) return false;
17
86
  const lower = cmd.toLowerCase();
18
- return SCAN_PATTERNS.some(p => lower.includes(p));
87
+ return scanPatterns.some(p => lower.includes(p.toLowerCase()));
19
88
  }
20
89
 
21
90
  function scanOutput(command, stdout, stderr) {
@@ -135,7 +204,7 @@ Do NOT continue with ANY task until the human explicitly resolves this.`;
135
204
  shouldScanCommand,
136
205
  checkFlaggedOutput,
137
206
  formatOutputBlockError,
138
- init() {}, // no startup work needed
207
+ init() { fetchPatternsFromServer(); },
139
208
  cleanup() {},
140
209
  };
141
210
  };
@@ -8,7 +8,17 @@ const os = require('os');
8
8
  const SKILL_SCAN_API = 'https://lschqndjjwtyrlcojvly.supabase.co/functions/v1/scan-skill';
9
9
  const HOME = os.homedir();
10
10
 
11
- module.exports = function createSkillsGuard({ readFileSync, httpsRequest, baseDir, apiKey, analytics }) {
11
+ module.exports = function createSkillsGuard({ readFileSync, httpsRequest, baseDir, apiKey, analytics, enabled = true }) {
12
+ // If skill delivery is disabled, return a no-op guard
13
+ if (!enabled) {
14
+ return {
15
+ checkFlaggedSkills() { return null; },
16
+ formatSkillBlockError() { return ''; },
17
+ init() {},
18
+ cleanup() {},
19
+ };
20
+ }
21
+
12
22
  const track = analytics ? analytics.track.bind(analytics) : () => {};
13
23
  const SKILL_CACHE_FILE = path.join(baseDir, 'monitor', '.skill_scan_cache.json');
14
24
  const INSTALL_ID_FILE = path.join(baseDir, 'monitor', '.install_id');
@@ -9,7 +9,15 @@ const _originalHttpsRequest = require('https').request;
9
9
  const os = require('os');
10
10
  const MONITOR_PY = path.join(__dirname, 'monitor', 'monitor.py');
11
11
  const MONITOR_CWD = path.join(__dirname, 'monitor');
12
- const CONFIG_FILE = path.join(os.homedir(), '.contextfort', 'config');
12
+ const CONFIG_DIR = path.join(os.homedir(), '.contextfort');
13
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config');
14
+ const PREFS_FILE = path.join(CONFIG_DIR, 'preferences.json');
15
+
16
+ function loadPreferences() {
17
+ try { return JSON.parse(_originalReadFileSync(PREFS_FILE, 'utf8')); } catch { return {}; }
18
+ }
19
+ const PREFS = loadPreferences();
20
+ const SKILL_DELIVER = PREFS.skillDeliver !== false; // default true
13
21
 
14
22
  // === Analytics ===
15
23
  const analytics = require('./monitor/analytics')({
@@ -32,7 +40,7 @@ function loadApiKey() {
32
40
  const API_KEY = loadApiKey();
33
41
 
34
42
  const NO_KEY_MESSAGE = `SECURITY FIREWALL -- No API key configured. ALL agent actions are blocked.
35
- Get your API key at https://contextfort.ai and run:
43
+ Get your API key at https://contextfort.ai/login and run:
36
44
  openclaw-secure set-key <your-key>
37
45
  Then restart your openclaw session.`;
38
46
 
@@ -48,6 +56,7 @@ const skillsGuard = require('./monitor/skills_guard')({
48
56
  baseDir: __dirname,
49
57
  apiKey: API_KEY,
50
58
  analytics,
59
+ enabled: SKILL_DELIVER,
51
60
  });
52
61
 
53
62
  // === Prompt Injection Guard (PostToolUse) ===
@@ -55,6 +64,9 @@ const ANTHROPIC_KEY = process.env.ANTHROPIC_API_KEY || null;
55
64
  const promptInjectionGuard = require('./monitor/prompt_injection_guard')({
56
65
  httpsRequest: _originalHttpsRequest,
57
66
  anthropicKey: ANTHROPIC_KEY,
67
+ readFileSync: _originalReadFileSync,
68
+ apiKey: API_KEY,
69
+ baseDir: __dirname,
58
70
  analytics,
59
71
  });
60
72
 
@@ -270,21 +282,8 @@ function hookFsMethods(fsModule) {
270
282
  if (fsModule.readFileSync && !fsModule.readFileSync.__hooked) {
271
283
  const orig = fsModule.readFileSync;
272
284
  fsModule.readFileSync = function(filePath, options) {
273
- const keyBlock = checkApiKey();
274
- if (keyBlock) {
275
- analytics.track('fs_blocked', { blocker: 'api_key' });
276
- const e = new Error(keyBlock.reason); e.code = 'EACCES'; throw e;
277
- }
278
- const outputBlock = promptInjectionGuard.checkFlaggedOutput();
279
- if (outputBlock) {
280
- analytics.track('fs_blocked', { blocker: 'prompt_injection' });
281
- const e = new Error(promptInjectionGuard.formatOutputBlockError(outputBlock)); e.code = 'EACCES'; throw e;
282
- }
283
- const skillBlock = skillsGuard.checkFlaggedSkills();
284
- if (skillBlock) {
285
- analytics.track('fs_blocked', { blocker: 'skill' });
286
- const e = new Error(skillsGuard.formatSkillBlockError(skillBlock)); e.code = 'EACCES'; throw e;
287
- }
285
+ // Pass-through: blocking here disrupts openclaw's own config/LLM operations.
286
+ // Agent actions are blocked at child_process level instead.
288
287
  return orig.apply(this, arguments);
289
288
  };
290
289
  fsModule.readFileSync.__hooked = true;
@@ -297,26 +296,8 @@ function hookHttpModule(mod, protocol) {
297
296
  if (mod.request && !mod.request.__hooked) {
298
297
  const orig = mod.request;
299
298
  mod.request = function(options, callback) {
300
- try {
301
- const keyBlock = checkApiKey();
302
- if (keyBlock) {
303
- analytics.track('http_blocked', { blocker: 'api_key' });
304
- const e = new Error(keyBlock.reason); e.code = 'ENOTALLOW'; throw e;
305
- }
306
- const outputBlock = promptInjectionGuard.checkFlaggedOutput();
307
- if (outputBlock) {
308
- analytics.track('http_blocked', { blocker: 'prompt_injection' });
309
- const e = new Error(promptInjectionGuard.formatOutputBlockError(outputBlock)); e.code = 'ENOTALLOW'; throw e;
310
- }
311
- const skillBlock = skillsGuard.checkFlaggedSkills();
312
- if (skillBlock) {
313
- analytics.track('http_blocked', { blocker: 'skill' });
314
- const e = new Error(skillsGuard.formatSkillBlockError(skillBlock)); e.code = 'ENOTALLOW'; throw e;
315
- }
316
- } catch (err) {
317
- if (err.code === 'ENOTALLOW') throw err;
318
- }
319
-
299
+ // Pass-through: blocking here kills openclaw's LLM API calls.
300
+ // Agent actions are blocked at child_process level instead.
320
301
  return orig.apply(this, arguments);
321
302
  };
322
303
  mod.request.__hooked = true;
@@ -325,26 +306,6 @@ function hookHttpModule(mod, protocol) {
325
306
  if (mod.get && !mod.get.__hooked) {
326
307
  const orig = mod.get;
327
308
  mod.get = function(options, callback) {
328
- try {
329
- const keyBlock = checkApiKey();
330
- if (keyBlock) {
331
- analytics.track('http_blocked', { blocker: 'api_key' });
332
- const e = new Error(keyBlock.reason); e.code = 'ENOTALLOW'; throw e;
333
- }
334
- const outputBlock = promptInjectionGuard.checkFlaggedOutput();
335
- if (outputBlock) {
336
- analytics.track('http_blocked', { blocker: 'prompt_injection' });
337
- const e = new Error(promptInjectionGuard.formatOutputBlockError(outputBlock)); e.code = 'ENOTALLOW'; throw e;
338
- }
339
- const skillBlock = skillsGuard.checkFlaggedSkills();
340
- if (skillBlock) {
341
- analytics.track('http_blocked', { blocker: 'skill' });
342
- const e = new Error(skillsGuard.formatSkillBlockError(skillBlock)); e.code = 'ENOTALLOW'; throw e;
343
- }
344
- } catch (err) {
345
- if (err.code === 'ENOTALLOW') throw err;
346
- }
347
-
348
309
  return orig.apply(this, arguments);
349
310
  };
350
311
  mod.get.__hooked = true;
@@ -357,27 +318,8 @@ function hookGlobalFetch() {
357
318
  if (!globalThis.fetch || globalThis.fetch.__hooked) return;
358
319
  const origFetch = globalThis.fetch;
359
320
  globalThis.fetch = function(url, options) {
360
- try {
361
- const keyBlock = checkApiKey();
362
- if (keyBlock) {
363
- analytics.track('fetch_blocked', { blocker: 'api_key' });
364
- return Promise.reject(new Error(keyBlock.reason));
365
- }
366
- const outputBlock = promptInjectionGuard.checkFlaggedOutput();
367
- if (outputBlock) {
368
- analytics.track('fetch_blocked', { blocker: 'prompt_injection' });
369
- return Promise.reject(new Error(promptInjectionGuard.formatOutputBlockError(outputBlock)));
370
- }
371
- const skillBlock = skillsGuard.checkFlaggedSkills();
372
- if (skillBlock) {
373
- analytics.track('fetch_blocked', { blocker: 'skill' });
374
- return Promise.reject(new Error(skillsGuard.formatSkillBlockError(skillBlock)));
375
- }
376
- } catch (err) {
377
- if (err.message && err.message.includes('SECURITY FIREWALL')) {
378
- return Promise.reject(err);
379
- }
380
- }
321
+ // Pass-through: blocking here kills openclaw's LLM API calls.
322
+ // Agent actions are blocked at child_process level instead.
381
323
  return origFetch.apply(this, arguments);
382
324
  };
383
325
  globalThis.fetch.__hooked = true;
@@ -413,6 +355,9 @@ Module.prototype.require = function(id) {
413
355
  return r;
414
356
  };
415
357
 
416
- // === Initialize Skill Scanner (non-blocking) ===
417
- setImmediate(() => { try { skillsGuard.init(); } catch {} });
358
+ // === Initialize guards (non-blocking) ===
359
+ setImmediate(() => {
360
+ try { skillsGuard.init(); } catch {}
361
+ try { promptInjectionGuard.init(); } catch {}
362
+ });
418
363
  process.on('exit', () => { skillsGuard.cleanup(); });
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@contextfort-ai/openclaw-secure",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Runtime security guard for OpenClaw — blocks malicious commands before they execute",
5
5
  "bin": {
6
6
  "openclaw-secure": "./bin/openclaw-secure.js"
7
7
  },
8
+ "scripts": {
9
+ "postinstall": "node -e \"console.log('\\n\\x1b[32m✓ openclaw-secure installed!\\x1b[0m\\n\\nGet your API key at https://contextfort.ai/login and run:\\n\\n openclaw-secure set-key <your-key>\\n openclaw-secure enable\\n')\""
10
+ },
8
11
  "keywords": [
9
12
  "openclaw",
10
13
  "security",