openclaw-safeclaw-plugin 0.7.2 → 0.8.1

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/README.md CHANGED
@@ -56,7 +56,7 @@ Set via environment variables or `~/.safeclaw/config.json`:
56
56
  |----------|---------|-------------|
57
57
  | `SAFECLAW_URL` | `https://api.safeclaw.eu/api/v1` | SafeClaw service URL |
58
58
  | `SAFECLAW_API_KEY` | *(empty)* | API key (set automatically by `safeclaw connect`) |
59
- | `SAFECLAW_TIMEOUT_MS` | `500` | Request timeout in ms |
59
+ | `SAFECLAW_TIMEOUT_MS` | `5000` | Request timeout in ms |
60
60
  | `SAFECLAW_ENABLED` | `true` | Set `false` to disable |
61
61
  | `SAFECLAW_ENFORCEMENT` | `enforce` | `enforce`, `warn-only`, `audit-only`, or `disabled` |
62
62
  | `SAFECLAW_FAIL_MODE` | `open` | `open` (allow on failure) or `closed` (block on failure) |
package/cli.tsx CHANGED
@@ -2,38 +2,90 @@
2
2
  import React from 'react';
3
3
  import { render } from 'ink';
4
4
  import { execSync } from 'child_process';
5
- import { readFileSync, writeFileSync, mkdirSync, chmodSync, existsSync } from 'fs';
6
- import { join } from 'path';
5
+ import { readFileSync, writeFileSync, mkdirSync, chmodSync, existsSync, copyFileSync, lstatSync, unlinkSync, rmSync } from 'fs';
6
+ import { join, dirname } from 'path';
7
7
  import { homedir } from 'os';
8
+ import { fileURLToPath } from 'url';
8
9
  import App from './tui/App.js';
10
+ import { loadConfig } from './tui/config.js';
9
11
 
10
- function registerWithOpenClaw(): boolean {
11
- // Use OpenClaw's native plugin install mechanism
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+
14
+ function readJson(path: string): Record<string, unknown> {
12
15
  try {
13
- execSync('openclaw plugins install openclaw-safeclaw-plugin', {
14
- encoding: 'utf-8',
15
- timeout: 30000,
16
- stdio: 'pipe',
17
- });
18
- return true;
16
+ return JSON.parse(readFileSync(path, 'utf-8'));
19
17
  } catch {
20
- // Fallback: try linking from the global npm install location
21
- try {
22
- const globalRoot = execSync('npm root -g', { encoding: 'utf-8', timeout: 5000 }).trim();
23
- const pluginPath = join(globalRoot, 'openclaw-safeclaw-plugin');
24
- if (existsSync(pluginPath)) {
25
- execSync(`openclaw plugins install --link "${pluginPath}"`, {
26
- encoding: 'utf-8',
27
- timeout: 15000,
28
- stdio: 'pipe',
29
- });
30
- return true;
18
+ return {};
19
+ }
20
+ }
21
+
22
+ function registerWithOpenClaw(): boolean {
23
+ const pluginRoot = join(__dirname, '..'); // one level up from dist/
24
+ const extensionDir = join(homedir(), '.openclaw', 'extensions', 'safeclaw');
25
+ const entryPoint = join(pluginRoot, 'dist', 'index.js');
26
+ const manifestSrc = join(pluginRoot, 'openclaw.plugin.json');
27
+
28
+ // Clean up stale symlink if it exists
29
+ try {
30
+ if (existsSync(extensionDir)) {
31
+ const stat = lstatSync(extensionDir);
32
+ if (stat.isSymbolicLink()) {
33
+ unlinkSync(extensionDir);
31
34
  }
32
- } catch {
33
- // Both methods failed
34
35
  }
36
+ } catch { /* ignore */ }
37
+
38
+ // Create extension directory with manifest + loader that imports from npm install
39
+ try {
40
+ mkdirSync(extensionDir, { recursive: true });
41
+
42
+ // Copy the manifest
43
+ if (existsSync(manifestSrc)) {
44
+ copyFileSync(manifestSrc, join(extensionDir, 'openclaw.plugin.json'));
45
+ } else {
46
+ // Write manifest inline if source file missing
47
+ writeFileSync(join(extensionDir, 'openclaw.plugin.json'), JSON.stringify({
48
+ id: 'safeclaw',
49
+ name: 'SafeClaw Neurosymbolic Governance',
50
+ configSchema: { type: 'object', additionalProperties: false, properties: {} },
51
+ }, null, 2) + '\n');
52
+ }
53
+
54
+ // Create index.js that loads from the actual npm install location
55
+ writeFileSync(join(extensionDir, 'index.js'),
56
+ `export { default } from '${entryPoint}';\n`);
57
+ } catch (e) {
58
+ console.warn(`Warning: Could not create extension: ${e instanceof Error ? e.message : e}`);
59
+ return false;
60
+ }
61
+
62
+ // Enable plugin in ~/.openclaw/openclaw.json
63
+ const openclawConfigPath = join(homedir(), '.openclaw', 'openclaw.json');
64
+ const ocConfig = readJson(openclawConfigPath);
65
+
66
+ if (!ocConfig.plugins || typeof ocConfig.plugins !== 'object') {
67
+ ocConfig.plugins = {};
68
+ }
69
+ const plugins = ocConfig.plugins as Record<string, unknown>;
70
+ if (!plugins.entries || typeof plugins.entries !== 'object') {
71
+ plugins.entries = {};
72
+ }
73
+ const entries = plugins.entries as Record<string, unknown>;
74
+
75
+ if (!entries.safeclaw || typeof entries.safeclaw !== 'object') {
76
+ entries.safeclaw = { enabled: true };
77
+ } else {
78
+ (entries.safeclaw as Record<string, unknown>).enabled = true;
79
+ }
80
+
81
+ try {
82
+ writeFileSync(openclawConfigPath, JSON.stringify(ocConfig, null, 2) + '\n');
83
+ } catch (e) {
84
+ console.warn(`Warning: Could not update OpenClaw config: ${e instanceof Error ? e.message : e}`);
85
+ return false;
35
86
  }
36
- return false;
87
+
88
+ return true;
37
89
  }
38
90
 
39
91
  const args = process.argv.slice(2);
@@ -125,9 +177,15 @@ if (command === 'connect') {
125
177
  console.log('Try: openclaw plugins install openclaw-safeclaw-plugin');
126
178
  }
127
179
  } else if (command === 'status') {
180
+ const cfg = loadConfig();
128
181
  const configPath = join(homedir(), '.safeclaw', 'config.json');
129
182
  let allOk = true;
130
183
 
184
+ // 0. Active config summary
185
+ console.log(`Config: enforcement=${cfg.enforcement}, failMode=${cfg.failMode}, timeout=${cfg.timeoutMs}ms`);
186
+ console.log(`Service: ${cfg.serviceUrl}`);
187
+ console.log('');
188
+
131
189
  // 1. Config file
132
190
  if (existsSync(configPath)) {
133
191
  console.log('[ok] Config file: ' + configPath);
@@ -137,20 +195,9 @@ if (command === 'connect') {
137
195
  }
138
196
 
139
197
  // 2. API key
140
- let apiKey = '';
141
- let serviceUrl = 'https://api.safeclaw.eu/api/v1';
142
- if (existsSync(configPath)) {
143
- try {
144
- const cfg = JSON.parse(readFileSync(configPath, 'utf-8'));
145
- const remote = cfg.remote as Record<string, string> | undefined;
146
- apiKey = remote?.apiKey ?? '';
147
- serviceUrl = remote?.serviceUrl ?? serviceUrl;
148
- } catch { /* ignore */ }
149
- }
150
-
151
- if (apiKey && apiKey.startsWith('sc_')) {
198
+ if (cfg.apiKey && cfg.apiKey.startsWith('sc_')) {
152
199
  console.log('[ok] API key: configured (sc_...)');
153
- } else if (apiKey) {
200
+ } else if (cfg.apiKey) {
154
201
  console.log('[!!] API key: invalid (must start with sc_)');
155
202
  allOk = false;
156
203
  } else {
@@ -158,25 +205,64 @@ if (command === 'connect') {
158
205
  allOk = false;
159
206
  }
160
207
 
161
- // 3. SafeClaw service reachable
208
+ // 3. SafeClaw service — health check (uses same timeout as plugin)
209
+ let serviceHealthy = false;
162
210
  try {
163
- const res = await fetch(`${serviceUrl}/health`, {
164
- signal: AbortSignal.timeout(5000),
165
- headers: apiKey ? { 'Authorization': `Bearer ${apiKey}` } : {},
211
+ const res = await fetch(`${cfg.serviceUrl}/health`, {
212
+ signal: AbortSignal.timeout(cfg.timeoutMs),
213
+ headers: cfg.apiKey ? { 'Authorization': `Bearer ${cfg.apiKey}` } : {},
166
214
  });
167
215
  if (res.ok) {
168
216
  const data = await res.json() as Record<string, unknown>;
169
- console.log(`[ok] SafeClaw service: ${data.status ?? 'ok'} (${serviceUrl})`);
217
+ console.log(`[ok] Service health: ${data.status ?? 'ok'}`);
218
+ serviceHealthy = true;
170
219
  } else {
171
- console.log(`[!!] SafeClaw service: HTTP ${res.status} (${serviceUrl})`);
220
+ console.log(`[!!] Service health: HTTP ${res.status}`);
172
221
  allOk = false;
173
222
  }
174
- } catch {
175
- console.log(`[!!] SafeClaw service: unreachable (${serviceUrl})`);
223
+ } catch (e) {
224
+ const isTimeout = e instanceof DOMException && e.name === 'TimeoutError';
225
+ console.log(`[!!] Service health: ${isTimeout ? `timeout after ${cfg.timeoutMs}ms` : 'unreachable'}`);
176
226
  allOk = false;
177
227
  }
178
228
 
179
- // 4. OpenClaw installed
229
+ // 4. SafeClaw service — evaluate endpoint (the actual gate)
230
+ if (serviceHealthy) {
231
+ try {
232
+ const res = await fetch(`${cfg.serviceUrl}/evaluate/tool-call`, {
233
+ method: 'POST',
234
+ headers: {
235
+ 'Content-Type': 'application/json',
236
+ ...(cfg.apiKey ? { 'Authorization': `Bearer ${cfg.apiKey}` } : {}),
237
+ },
238
+ body: JSON.stringify({
239
+ sessionId: 'status-check',
240
+ userId: 'status-check',
241
+ toolName: 'echo',
242
+ params: { message: 'status-check' },
243
+ }),
244
+ signal: AbortSignal.timeout(cfg.timeoutMs),
245
+ });
246
+ if (res.ok || res.status === 422) {
247
+ // 422 = validation error is fine — means the service is processing requests
248
+ console.log('[ok] Service evaluate: responding');
249
+ } else if (res.status === 401 || res.status === 403) {
250
+ console.log('[ok] Service evaluate: responding (auth required)');
251
+ } else {
252
+ console.log(`[!!] Service evaluate: HTTP ${res.status}`);
253
+ allOk = false;
254
+ }
255
+ } catch (e) {
256
+ const isTimeout = e instanceof DOMException && e.name === 'TimeoutError';
257
+ console.log(`[!!] Service evaluate: ${isTimeout ? `timeout after ${cfg.timeoutMs}ms` : 'unreachable'}`);
258
+ if (cfg.failMode === 'closed') {
259
+ console.log(' ↳ failMode=closed means ALL tool calls will be blocked!');
260
+ }
261
+ allOk = false;
262
+ }
263
+ }
264
+
265
+ // 5. OpenClaw installed
180
266
  try {
181
267
  execSync('which openclaw', { encoding: 'utf-8', stdio: 'pipe' });
182
268
  console.log('[ok] OpenClaw: installed');
@@ -185,21 +271,41 @@ if (command === 'connect') {
185
271
  allOk = false;
186
272
  }
187
273
 
188
- // 5. Plugin registered with OpenClaw
189
- try {
190
- const pluginList = execSync('openclaw plugins list', {
191
- encoding: 'utf-8',
192
- timeout: 10000,
193
- stdio: 'pipe',
194
- });
195
- if (pluginList.includes('safeclaw')) {
196
- console.log('[ok] Plugin: registered with OpenClaw');
274
+ // 6. Plugin extension files exist
275
+ const extensionDir = join(homedir(), '.openclaw', 'extensions', 'safeclaw');
276
+ const hasManifest = existsSync(join(extensionDir, 'openclaw.plugin.json'));
277
+ const hasEntry = existsSync(join(extensionDir, 'index.js'));
278
+ if (hasManifest && hasEntry) {
279
+ console.log('[ok] Plugin files: ' + extensionDir);
280
+ } else if (existsSync(extensionDir)) {
281
+ const stat = lstatSync(extensionDir);
282
+ if (stat.isSymbolicLink()) {
283
+ console.log('[!!] Plugin: stale symlink (run safeclaw setup to fix)');
197
284
  } else {
198
- console.log('[!!] Plugin: not registered with OpenClaw. Run: safeclaw setup');
285
+ console.log('[!!] Plugin: missing files in ' + extensionDir);
286
+ }
287
+ allOk = false;
288
+ } else {
289
+ console.log('[!!] Plugin: not installed. Run: safeclaw setup');
290
+ allOk = false;
291
+ }
292
+
293
+ // 7. Plugin enabled in OpenClaw config
294
+ const ocConfigPath = join(homedir(), '.openclaw', 'openclaw.json');
295
+ if (existsSync(ocConfigPath)) {
296
+ const ocConfig = readJson(ocConfigPath);
297
+ const plugins = ocConfig.plugins as Record<string, unknown> | undefined;
298
+ const entries = plugins?.entries as Record<string, unknown> | undefined;
299
+ const safeclaw = entries?.safeclaw as Record<string, unknown> | undefined;
300
+ if (safeclaw?.enabled) {
301
+ console.log('[ok] OpenClaw config: safeclaw enabled');
302
+ } else {
303
+ console.log('[!!] OpenClaw config: safeclaw not enabled');
199
304
  allOk = false;
200
305
  }
201
- } catch {
202
- console.log('[??] Plugin: could not check OpenClaw plugin list');
306
+ } else {
307
+ console.log('[!!] OpenClaw config: not found');
308
+ allOk = false;
203
309
  }
204
310
 
205
311
  // Summary
package/dist/cli.js CHANGED
@@ -2,39 +2,83 @@
2
2
  import React from 'react';
3
3
  import { render } from 'ink';
4
4
  import { execSync } from 'child_process';
5
- import { readFileSync, writeFileSync, mkdirSync, chmodSync, existsSync } from 'fs';
6
- import { join } from 'path';
5
+ import { readFileSync, writeFileSync, mkdirSync, chmodSync, existsSync, copyFileSync, lstatSync, unlinkSync } from 'fs';
6
+ import { join, dirname } from 'path';
7
7
  import { homedir } from 'os';
8
+ import { fileURLToPath } from 'url';
8
9
  import App from './tui/App.js';
9
- function registerWithOpenClaw() {
10
- // Use OpenClaw's native plugin install mechanism
10
+ import { loadConfig } from './tui/config.js';
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+ function readJson(path) {
11
13
  try {
12
- execSync('openclaw plugins install openclaw-safeclaw-plugin', {
13
- encoding: 'utf-8',
14
- timeout: 30000,
15
- stdio: 'pipe',
16
- });
17
- return true;
14
+ return JSON.parse(readFileSync(path, 'utf-8'));
18
15
  }
19
16
  catch {
20
- // Fallback: try linking from the global npm install location
21
- try {
22
- const globalRoot = execSync('npm root -g', { encoding: 'utf-8', timeout: 5000 }).trim();
23
- const pluginPath = join(globalRoot, 'openclaw-safeclaw-plugin');
24
- if (existsSync(pluginPath)) {
25
- execSync(`openclaw plugins install --link "${pluginPath}"`, {
26
- encoding: 'utf-8',
27
- timeout: 15000,
28
- stdio: 'pipe',
29
- });
30
- return true;
17
+ return {};
18
+ }
19
+ }
20
+ function registerWithOpenClaw() {
21
+ const pluginRoot = join(__dirname, '..'); // one level up from dist/
22
+ const extensionDir = join(homedir(), '.openclaw', 'extensions', 'safeclaw');
23
+ const entryPoint = join(pluginRoot, 'dist', 'index.js');
24
+ const manifestSrc = join(pluginRoot, 'openclaw.plugin.json');
25
+ // Clean up stale symlink if it exists
26
+ try {
27
+ if (existsSync(extensionDir)) {
28
+ const stat = lstatSync(extensionDir);
29
+ if (stat.isSymbolicLink()) {
30
+ unlinkSync(extensionDir);
31
31
  }
32
32
  }
33
- catch {
34
- // Both methods failed
33
+ }
34
+ catch { /* ignore */ }
35
+ // Create extension directory with manifest + loader that imports from npm install
36
+ try {
37
+ mkdirSync(extensionDir, { recursive: true });
38
+ // Copy the manifest
39
+ if (existsSync(manifestSrc)) {
40
+ copyFileSync(manifestSrc, join(extensionDir, 'openclaw.plugin.json'));
35
41
  }
42
+ else {
43
+ // Write manifest inline if source file missing
44
+ writeFileSync(join(extensionDir, 'openclaw.plugin.json'), JSON.stringify({
45
+ id: 'safeclaw',
46
+ name: 'SafeClaw Neurosymbolic Governance',
47
+ configSchema: { type: 'object', additionalProperties: false, properties: {} },
48
+ }, null, 2) + '\n');
49
+ }
50
+ // Create index.js that loads from the actual npm install location
51
+ writeFileSync(join(extensionDir, 'index.js'), `export { default } from '${entryPoint}';\n`);
52
+ }
53
+ catch (e) {
54
+ console.warn(`Warning: Could not create extension: ${e instanceof Error ? e.message : e}`);
55
+ return false;
36
56
  }
37
- return false;
57
+ // Enable plugin in ~/.openclaw/openclaw.json
58
+ const openclawConfigPath = join(homedir(), '.openclaw', 'openclaw.json');
59
+ const ocConfig = readJson(openclawConfigPath);
60
+ if (!ocConfig.plugins || typeof ocConfig.plugins !== 'object') {
61
+ ocConfig.plugins = {};
62
+ }
63
+ const plugins = ocConfig.plugins;
64
+ if (!plugins.entries || typeof plugins.entries !== 'object') {
65
+ plugins.entries = {};
66
+ }
67
+ const entries = plugins.entries;
68
+ if (!entries.safeclaw || typeof entries.safeclaw !== 'object') {
69
+ entries.safeclaw = { enabled: true };
70
+ }
71
+ else {
72
+ entries.safeclaw.enabled = true;
73
+ }
74
+ try {
75
+ writeFileSync(openclawConfigPath, JSON.stringify(ocConfig, null, 2) + '\n');
76
+ }
77
+ catch (e) {
78
+ console.warn(`Warning: Could not update OpenClaw config: ${e instanceof Error ? e.message : e}`);
79
+ return false;
80
+ }
81
+ return true;
38
82
  }
39
83
  const args = process.argv.slice(2);
40
84
  const command = args[0];
@@ -124,8 +168,13 @@ else if (command === 'setup') {
124
168
  }
125
169
  }
126
170
  else if (command === 'status') {
171
+ const cfg = loadConfig();
127
172
  const configPath = join(homedir(), '.safeclaw', 'config.json');
128
173
  let allOk = true;
174
+ // 0. Active config summary
175
+ console.log(`Config: enforcement=${cfg.enforcement}, failMode=${cfg.failMode}, timeout=${cfg.timeoutMs}ms`);
176
+ console.log(`Service: ${cfg.serviceUrl}`);
177
+ console.log('');
129
178
  // 1. Config file
130
179
  if (existsSync(configPath)) {
131
180
  console.log('[ok] Config file: ' + configPath);
@@ -135,21 +184,10 @@ else if (command === 'status') {
135
184
  allOk = false;
136
185
  }
137
186
  // 2. API key
138
- let apiKey = '';
139
- let serviceUrl = 'https://api.safeclaw.eu/api/v1';
140
- if (existsSync(configPath)) {
141
- try {
142
- const cfg = JSON.parse(readFileSync(configPath, 'utf-8'));
143
- const remote = cfg.remote;
144
- apiKey = remote?.apiKey ?? '';
145
- serviceUrl = remote?.serviceUrl ?? serviceUrl;
146
- }
147
- catch { /* ignore */ }
148
- }
149
- if (apiKey && apiKey.startsWith('sc_')) {
187
+ if (cfg.apiKey && cfg.apiKey.startsWith('sc_')) {
150
188
  console.log('[ok] API key: configured (sc_...)');
151
189
  }
152
- else if (apiKey) {
190
+ else if (cfg.apiKey) {
153
191
  console.log('[!!] API key: invalid (must start with sc_)');
154
192
  allOk = false;
155
193
  }
@@ -157,26 +195,67 @@ else if (command === 'status') {
157
195
  console.log('[!!] API key: not set. Run: safeclaw connect <api-key>');
158
196
  allOk = false;
159
197
  }
160
- // 3. SafeClaw service reachable
198
+ // 3. SafeClaw service — health check (uses same timeout as plugin)
199
+ let serviceHealthy = false;
161
200
  try {
162
- const res = await fetch(`${serviceUrl}/health`, {
163
- signal: AbortSignal.timeout(5000),
164
- headers: apiKey ? { 'Authorization': `Bearer ${apiKey}` } : {},
201
+ const res = await fetch(`${cfg.serviceUrl}/health`, {
202
+ signal: AbortSignal.timeout(cfg.timeoutMs),
203
+ headers: cfg.apiKey ? { 'Authorization': `Bearer ${cfg.apiKey}` } : {},
165
204
  });
166
205
  if (res.ok) {
167
206
  const data = await res.json();
168
- console.log(`[ok] SafeClaw service: ${data.status ?? 'ok'} (${serviceUrl})`);
207
+ console.log(`[ok] Service health: ${data.status ?? 'ok'}`);
208
+ serviceHealthy = true;
169
209
  }
170
210
  else {
171
- console.log(`[!!] SafeClaw service: HTTP ${res.status} (${serviceUrl})`);
211
+ console.log(`[!!] Service health: HTTP ${res.status}`);
172
212
  allOk = false;
173
213
  }
174
214
  }
175
- catch {
176
- console.log(`[!!] SafeClaw service: unreachable (${serviceUrl})`);
215
+ catch (e) {
216
+ const isTimeout = e instanceof DOMException && e.name === 'TimeoutError';
217
+ console.log(`[!!] Service health: ${isTimeout ? `timeout after ${cfg.timeoutMs}ms` : 'unreachable'}`);
177
218
  allOk = false;
178
219
  }
179
- // 4. OpenClaw installed
220
+ // 4. SafeClaw service — evaluate endpoint (the actual gate)
221
+ if (serviceHealthy) {
222
+ try {
223
+ const res = await fetch(`${cfg.serviceUrl}/evaluate/tool-call`, {
224
+ method: 'POST',
225
+ headers: {
226
+ 'Content-Type': 'application/json',
227
+ ...(cfg.apiKey ? { 'Authorization': `Bearer ${cfg.apiKey}` } : {}),
228
+ },
229
+ body: JSON.stringify({
230
+ sessionId: 'status-check',
231
+ userId: 'status-check',
232
+ toolName: 'echo',
233
+ params: { message: 'status-check' },
234
+ }),
235
+ signal: AbortSignal.timeout(cfg.timeoutMs),
236
+ });
237
+ if (res.ok || res.status === 422) {
238
+ // 422 = validation error is fine — means the service is processing requests
239
+ console.log('[ok] Service evaluate: responding');
240
+ }
241
+ else if (res.status === 401 || res.status === 403) {
242
+ console.log('[ok] Service evaluate: responding (auth required)');
243
+ }
244
+ else {
245
+ console.log(`[!!] Service evaluate: HTTP ${res.status}`);
246
+ allOk = false;
247
+ }
248
+ }
249
+ catch (e) {
250
+ const isTimeout = e instanceof DOMException && e.name === 'TimeoutError';
251
+ console.log(`[!!] Service evaluate: ${isTimeout ? `timeout after ${cfg.timeoutMs}ms` : 'unreachable'}`);
252
+ if (cfg.failMode === 'closed') {
253
+ console.log(' ↳ failMode=closed means ALL tool calls will be blocked!');
254
+ }
255
+ allOk = false;
256
+ }
257
+ }
258
+ // 5. OpenClaw installed
180
259
  try {
181
260
  execSync('which openclaw', { encoding: 'utf-8', stdio: 'pipe' });
182
261
  console.log('[ok] OpenClaw: installed');
@@ -185,23 +264,45 @@ else if (command === 'status') {
185
264
  console.log('[!!] OpenClaw: not found in PATH');
186
265
  allOk = false;
187
266
  }
188
- // 5. Plugin registered with OpenClaw
189
- try {
190
- const pluginList = execSync('openclaw plugins list', {
191
- encoding: 'utf-8',
192
- timeout: 10000,
193
- stdio: 'pipe',
194
- });
195
- if (pluginList.includes('safeclaw')) {
196
- console.log('[ok] Plugin: registered with OpenClaw');
267
+ // 6. Plugin extension files exist
268
+ const extensionDir = join(homedir(), '.openclaw', 'extensions', 'safeclaw');
269
+ const hasManifest = existsSync(join(extensionDir, 'openclaw.plugin.json'));
270
+ const hasEntry = existsSync(join(extensionDir, 'index.js'));
271
+ if (hasManifest && hasEntry) {
272
+ console.log('[ok] Plugin files: ' + extensionDir);
273
+ }
274
+ else if (existsSync(extensionDir)) {
275
+ const stat = lstatSync(extensionDir);
276
+ if (stat.isSymbolicLink()) {
277
+ console.log('[!!] Plugin: stale symlink (run safeclaw setup to fix)');
278
+ }
279
+ else {
280
+ console.log('[!!] Plugin: missing files in ' + extensionDir);
281
+ }
282
+ allOk = false;
283
+ }
284
+ else {
285
+ console.log('[!!] Plugin: not installed. Run: safeclaw setup');
286
+ allOk = false;
287
+ }
288
+ // 7. Plugin enabled in OpenClaw config
289
+ const ocConfigPath = join(homedir(), '.openclaw', 'openclaw.json');
290
+ if (existsSync(ocConfigPath)) {
291
+ const ocConfig = readJson(ocConfigPath);
292
+ const plugins = ocConfig.plugins;
293
+ const entries = plugins?.entries;
294
+ const safeclaw = entries?.safeclaw;
295
+ if (safeclaw?.enabled) {
296
+ console.log('[ok] OpenClaw config: safeclaw enabled');
197
297
  }
198
298
  else {
199
- console.log('[!!] Plugin: not registered with OpenClaw. Run: safeclaw setup');
299
+ console.log('[!!] OpenClaw config: safeclaw not enabled');
200
300
  allOk = false;
201
301
  }
202
302
  }
203
- catch {
204
- console.log('[??] Plugin: could not check OpenClaw plugin list');
303
+ else {
304
+ console.log('[!!] OpenClaw config: not found');
305
+ allOk = false;
205
306
  }
206
307
  // Summary
207
308
  console.log('');
@@ -16,10 +16,10 @@ export function loadConfig() {
16
16
  const defaults = {
17
17
  serviceUrl: 'https://api.safeclaw.eu/api/v1',
18
18
  apiKey: '',
19
- timeoutMs: 500,
19
+ timeoutMs: 5000,
20
20
  enabled: true,
21
21
  enforcement: 'enforce',
22
- failMode: 'closed',
22
+ failMode: 'open',
23
23
  agentId: '',
24
24
  agentToken: '',
25
25
  };
@@ -73,8 +73,8 @@ export function loadConfig() {
73
73
  }
74
74
  const validFailModes = ['open', 'closed'];
75
75
  if (!validFailModes.includes(defaults.failMode)) {
76
- console.warn(`[SafeClaw] Invalid fail mode "${defaults.failMode}", defaulting to "closed"`);
77
- defaults.failMode = 'closed';
76
+ console.warn(`[SafeClaw] Invalid fail mode "${defaults.failMode}", defaulting to "open"`);
77
+ defaults.failMode = 'open';
78
78
  }
79
79
  return defaults;
80
80
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-safeclaw-plugin",
3
- "version": "0.7.2",
3
+ "version": "0.8.1",
4
4
  "description": "SafeClaw Neurosymbolic Governance plugin for OpenClaw — validates AI agent actions against OWL ontologies and SHACL constraints",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/tui/config.ts CHANGED
@@ -34,10 +34,10 @@ export function loadConfig(): SafeClawConfig {
34
34
  const defaults: SafeClawConfig = {
35
35
  serviceUrl: 'https://api.safeclaw.eu/api/v1',
36
36
  apiKey: '',
37
- timeoutMs: 500,
37
+ timeoutMs: 5000,
38
38
  enabled: true,
39
39
  enforcement: 'enforce',
40
- failMode: 'closed',
40
+ failMode: 'open',
41
41
  agentId: '',
42
42
  agentToken: '',
43
43
  };
@@ -79,8 +79,8 @@ export function loadConfig(): SafeClawConfig {
79
79
 
80
80
  const validFailModes = ['open', 'closed'] as const;
81
81
  if (!validFailModes.includes(defaults.failMode as any)) {
82
- console.warn(`[SafeClaw] Invalid fail mode "${defaults.failMode}", defaulting to "closed"`);
83
- defaults.failMode = 'closed';
82
+ console.warn(`[SafeClaw] Invalid fail mode "${defaults.failMode}", defaulting to "open"`);
83
+ defaults.failMode = 'open';
84
84
  }
85
85
 
86
86
  return defaults;