openclaw-safeclaw-plugin 0.8.0 → 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 +1 -1
- package/cli.tsx +59 -24
- package/dist/cli.js +60 -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,64 @@ 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
|
+
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
|
|
231
266
|
try {
|
|
232
267
|
execSync('which openclaw', { encoding: 'utf-8', stdio: 'pipe' });
|
|
233
268
|
console.log('[ok] OpenClaw: installed');
|
|
@@ -236,7 +271,7 @@ if (command === 'connect') {
|
|
|
236
271
|
allOk = false;
|
|
237
272
|
}
|
|
238
273
|
|
|
239
|
-
//
|
|
274
|
+
// 6. Plugin extension files exist
|
|
240
275
|
const extensionDir = join(homedir(), '.openclaw', 'extensions', 'safeclaw');
|
|
241
276
|
const hasManifest = existsSync(join(extensionDir, 'openclaw.plugin.json'));
|
|
242
277
|
const hasEntry = existsSync(join(extensionDir, 'index.js'));
|
|
@@ -255,7 +290,7 @@ if (command === 'connect') {
|
|
|
255
290
|
allOk = false;
|
|
256
291
|
}
|
|
257
292
|
|
|
258
|
-
//
|
|
293
|
+
// 7. Plugin enabled in OpenClaw config
|
|
259
294
|
const ocConfigPath = join(homedir(), '.openclaw', 'openclaw.json');
|
|
260
295
|
if (existsSync(ocConfigPath)) {
|
|
261
296
|
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,67 @@ 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
|
+
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
|
|
223
259
|
try {
|
|
224
260
|
execSync('which openclaw', { encoding: 'utf-8', stdio: 'pipe' });
|
|
225
261
|
console.log('[ok] OpenClaw: installed');
|
|
@@ -228,7 +264,7 @@ else if (command === 'status') {
|
|
|
228
264
|
console.log('[!!] OpenClaw: not found in PATH');
|
|
229
265
|
allOk = false;
|
|
230
266
|
}
|
|
231
|
-
//
|
|
267
|
+
// 6. Plugin extension files exist
|
|
232
268
|
const extensionDir = join(homedir(), '.openclaw', 'extensions', 'safeclaw');
|
|
233
269
|
const hasManifest = existsSync(join(extensionDir, 'openclaw.plugin.json'));
|
|
234
270
|
const hasEntry = existsSync(join(extensionDir, 'index.js'));
|
|
@@ -249,7 +285,7 @@ else if (command === 'status') {
|
|
|
249
285
|
console.log('[!!] Plugin: not installed. Run: safeclaw setup');
|
|
250
286
|
allOk = false;
|
|
251
287
|
}
|
|
252
|
-
//
|
|
288
|
+
// 7. Plugin enabled in OpenClaw config
|
|
253
289
|
const ocConfigPath = join(homedir(), '.openclaw', 'openclaw.json');
|
|
254
290
|
if (existsSync(ocConfigPath)) {
|
|
255
291
|
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.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:
|
|
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;
|