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 +1 -1
- package/cli.tsx +64 -24
- package/dist/cli.js +66 -24
- package/dist/tui/config.js +4 -4
- package/package.json +1 -1
- package/tui/config.ts +4 -4
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` | `
|
|
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
|
-
|
|
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
|
|
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(
|
|
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]
|
|
217
|
+
console.log(`[ok] Service health: ${data.status ?? 'ok'}`);
|
|
218
|
+
serviceHealthy = true;
|
|
221
219
|
} else {
|
|
222
|
-
console.log(`[!!]
|
|
220
|
+
console.log(`[!!] Service health: HTTP ${res.status}`);
|
|
223
221
|
allOk = false;
|
|
224
222
|
}
|
|
225
|
-
} catch {
|
|
226
|
-
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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(
|
|
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]
|
|
207
|
+
console.log(`[ok] Service health: ${data.status ?? 'ok'}`);
|
|
208
|
+
serviceHealthy = true;
|
|
212
209
|
}
|
|
213
210
|
else {
|
|
214
|
-
console.log(`[!!]
|
|
211
|
+
console.log(`[!!] Service health: HTTP ${res.status}`);
|
|
215
212
|
allOk = false;
|
|
216
213
|
}
|
|
217
214
|
}
|
|
218
|
-
catch {
|
|
219
|
-
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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);
|
package/dist/tui/config.js
CHANGED
|
@@ -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:
|
|
19
|
+
timeoutMs: 5000,
|
|
20
20
|
enabled: true,
|
|
21
21
|
enforcement: 'enforce',
|
|
22
|
-
failMode: '
|
|
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 "
|
|
77
|
-
defaults.failMode = '
|
|
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.
|
|
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:
|
|
37
|
+
timeoutMs: 5000,
|
|
38
38
|
enabled: true,
|
|
39
39
|
enforcement: 'enforce',
|
|
40
|
-
failMode: '
|
|
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 "
|
|
83
|
-
defaults.failMode = '
|
|
82
|
+
console.warn(`[SafeClaw] Invalid fail mode "${defaults.failMode}", defaulting to "open"`);
|
|
83
|
+
defaults.failMode = 'open';
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
return defaults;
|