openclaw-safeclaw-plugin 0.8.0 → 0.8.2

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
@@ -7,6 +7,7 @@ import { join, dirname } from 'path';
7
7
  import { homedir } from 'os';
8
8
  import { fileURLToPath } from 'url';
9
9
  import App from './tui/App.js';
10
+ import { loadConfig } from './tui/config.js';
10
11
 
11
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
13
 
@@ -176,9 +177,15 @@ if (command === 'connect') {
176
177
  console.log('Try: openclaw plugins install openclaw-safeclaw-plugin');
177
178
  }
178
179
  } else if (command === 'status') {
180
+ const cfg = loadConfig();
179
181
  const configPath = join(homedir(), '.safeclaw', 'config.json');
180
182
  let allOk = true;
181
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
+
182
189
  // 1. Config file
183
190
  if (existsSync(configPath)) {
184
191
  console.log('[ok] Config file: ' + configPath);
@@ -188,20 +195,9 @@ if (command === 'connect') {
188
195
  }
189
196
 
190
197
  // 2. API key
191
- let apiKey = '';
192
- let serviceUrl = 'https://api.safeclaw.eu/api/v1';
193
- if (existsSync(configPath)) {
194
- try {
195
- const cfg = JSON.parse(readFileSync(configPath, 'utf-8'));
196
- const remote = cfg.remote as Record<string, string> | undefined;
197
- apiKey = remote?.apiKey ?? '';
198
- serviceUrl = remote?.serviceUrl ?? serviceUrl;
199
- } catch { /* ignore */ }
200
- }
201
-
202
- if (apiKey && apiKey.startsWith('sc_')) {
198
+ if (cfg.apiKey && cfg.apiKey.startsWith('sc_')) {
203
199
  console.log('[ok] API key: configured (sc_...)');
204
- } else if (apiKey) {
200
+ } else if (cfg.apiKey) {
205
201
  console.log('[!!] API key: invalid (must start with sc_)');
206
202
  allOk = false;
207
203
  } else {
@@ -209,25 +205,69 @@ if (command === 'connect') {
209
205
  allOk = false;
210
206
  }
211
207
 
212
- // 3. SafeClaw service reachable
208
+ // 3. SafeClaw service — health check (uses same timeout as plugin)
209
+ let serviceHealthy = false;
213
210
  try {
214
- const res = await fetch(`${serviceUrl}/health`, {
215
- signal: AbortSignal.timeout(5000),
216
- 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}` } : {},
217
214
  });
218
215
  if (res.ok) {
219
216
  const data = await res.json() as Record<string, unknown>;
220
- console.log(`[ok] SafeClaw service: ${data.status ?? 'ok'} (${serviceUrl})`);
217
+ console.log(`[ok] Service health: ${data.status ?? 'ok'}`);
218
+ serviceHealthy = true;
221
219
  } else {
222
- console.log(`[!!] SafeClaw service: HTTP ${res.status} (${serviceUrl})`);
220
+ console.log(`[!!] Service health: HTTP ${res.status}`);
223
221
  allOk = false;
224
222
  }
225
- } catch {
226
- 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'}`);
227
226
  allOk = false;
228
227
  }
229
228
 
230
- // 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
+ let detail = '';
253
+ try {
254
+ const body = await res.json() as Record<string, unknown>;
255
+ detail = ` — ${body.detail ?? body.error ?? JSON.stringify(body)}`;
256
+ } catch { /* ignore */ }
257
+ console.log(`[!!] Service evaluate: HTTP ${res.status}${detail}`);
258
+ allOk = false;
259
+ }
260
+ } catch (e) {
261
+ const isTimeout = e instanceof DOMException && e.name === 'TimeoutError';
262
+ console.log(`[!!] Service evaluate: ${isTimeout ? `timeout after ${cfg.timeoutMs}ms` : 'unreachable'}`);
263
+ if (cfg.failMode === 'closed') {
264
+ console.log(' ↳ failMode=closed means ALL tool calls will be blocked!');
265
+ }
266
+ allOk = false;
267
+ }
268
+ }
269
+
270
+ // 5. OpenClaw installed
231
271
  try {
232
272
  execSync('which openclaw', { encoding: 'utf-8', stdio: 'pipe' });
233
273
  console.log('[ok] OpenClaw: installed');
@@ -236,7 +276,7 @@ if (command === 'connect') {
236
276
  allOk = false;
237
277
  }
238
278
 
239
- // 5. Plugin extension files exist
279
+ // 6. Plugin extension files exist
240
280
  const extensionDir = join(homedir(), '.openclaw', 'extensions', 'safeclaw');
241
281
  const hasManifest = existsSync(join(extensionDir, 'openclaw.plugin.json'));
242
282
  const hasEntry = existsSync(join(extensionDir, 'index.js'));
@@ -255,7 +295,7 @@ if (command === 'connect') {
255
295
  allOk = false;
256
296
  }
257
297
 
258
- // 6. Plugin enabled in OpenClaw config
298
+ // 7. Plugin enabled in OpenClaw config
259
299
  const ocConfigPath = join(homedir(), '.openclaw', 'openclaw.json');
260
300
  if (existsSync(ocConfigPath)) {
261
301
  const ocConfig = readJson(ocConfigPath);
package/dist/cli.js CHANGED
@@ -7,6 +7,7 @@ import { join, dirname } from 'path';
7
7
  import { homedir } from 'os';
8
8
  import { fileURLToPath } from 'url';
9
9
  import App from './tui/App.js';
10
+ import { loadConfig } from './tui/config.js';
10
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
12
  function readJson(path) {
12
13
  try {
@@ -167,8 +168,13 @@ else if (command === 'setup') {
167
168
  }
168
169
  }
169
170
  else if (command === 'status') {
171
+ const cfg = loadConfig();
170
172
  const configPath = join(homedir(), '.safeclaw', 'config.json');
171
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('');
172
178
  // 1. Config file
173
179
  if (existsSync(configPath)) {
174
180
  console.log('[ok] Config file: ' + configPath);
@@ -178,21 +184,10 @@ else if (command === 'status') {
178
184
  allOk = false;
179
185
  }
180
186
  // 2. API key
181
- let apiKey = '';
182
- let serviceUrl = 'https://api.safeclaw.eu/api/v1';
183
- if (existsSync(configPath)) {
184
- try {
185
- const cfg = JSON.parse(readFileSync(configPath, 'utf-8'));
186
- const remote = cfg.remote;
187
- apiKey = remote?.apiKey ?? '';
188
- serviceUrl = remote?.serviceUrl ?? serviceUrl;
189
- }
190
- catch { /* ignore */ }
191
- }
192
- if (apiKey && apiKey.startsWith('sc_')) {
187
+ if (cfg.apiKey && cfg.apiKey.startsWith('sc_')) {
193
188
  console.log('[ok] API key: configured (sc_...)');
194
189
  }
195
- else if (apiKey) {
190
+ else if (cfg.apiKey) {
196
191
  console.log('[!!] API key: invalid (must start with sc_)');
197
192
  allOk = false;
198
193
  }
@@ -200,26 +195,73 @@ else if (command === 'status') {
200
195
  console.log('[!!] API key: not set. Run: safeclaw connect <api-key>');
201
196
  allOk = false;
202
197
  }
203
- // 3. SafeClaw service reachable
198
+ // 3. SafeClaw service — health check (uses same timeout as plugin)
199
+ let serviceHealthy = false;
204
200
  try {
205
- const res = await fetch(`${serviceUrl}/health`, {
206
- signal: AbortSignal.timeout(5000),
207
- 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}` } : {},
208
204
  });
209
205
  if (res.ok) {
210
206
  const data = await res.json();
211
- console.log(`[ok] SafeClaw service: ${data.status ?? 'ok'} (${serviceUrl})`);
207
+ console.log(`[ok] Service health: ${data.status ?? 'ok'}`);
208
+ serviceHealthy = true;
212
209
  }
213
210
  else {
214
- console.log(`[!!] SafeClaw service: HTTP ${res.status} (${serviceUrl})`);
211
+ console.log(`[!!] Service health: HTTP ${res.status}`);
215
212
  allOk = false;
216
213
  }
217
214
  }
218
- catch {
219
- 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'}`);
220
218
  allOk = false;
221
219
  }
222
- // 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
+ let detail = '';
246
+ try {
247
+ const body = await res.json();
248
+ detail = ` — ${body.detail ?? body.error ?? JSON.stringify(body)}`;
249
+ }
250
+ catch { /* ignore */ }
251
+ console.log(`[!!] Service evaluate: HTTP ${res.status}${detail}`);
252
+ allOk = false;
253
+ }
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
+ // 5. OpenClaw installed
223
265
  try {
224
266
  execSync('which openclaw', { encoding: 'utf-8', stdio: 'pipe' });
225
267
  console.log('[ok] OpenClaw: installed');
@@ -228,7 +270,7 @@ else if (command === 'status') {
228
270
  console.log('[!!] OpenClaw: not found in PATH');
229
271
  allOk = false;
230
272
  }
231
- // 5. Plugin extension files exist
273
+ // 6. Plugin extension files exist
232
274
  const extensionDir = join(homedir(), '.openclaw', 'extensions', 'safeclaw');
233
275
  const hasManifest = existsSync(join(extensionDir, 'openclaw.plugin.json'));
234
276
  const hasEntry = existsSync(join(extensionDir, 'index.js'));
@@ -249,7 +291,7 @@ else if (command === 'status') {
249
291
  console.log('[!!] Plugin: not installed. Run: safeclaw setup');
250
292
  allOk = false;
251
293
  }
252
- // 6. Plugin enabled in OpenClaw config
294
+ // 7. Plugin enabled in OpenClaw config
253
295
  const ocConfigPath = join(homedir(), '.openclaw', 'openclaw.json');
254
296
  if (existsSync(ocConfigPath)) {
255
297
  const ocConfig = readJson(ocConfigPath);
@@ -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.8.0",
3
+ "version": "0.8.2",
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;