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 +1 -1
- package/cli.tsx +164 -58
- package/dist/cli.js +159 -58
- 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
|
@@ -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
|
-
|
|
11
|
-
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
|
|
14
|
+
function readJson(path: string): Record<string, unknown> {
|
|
12
15
|
try {
|
|
13
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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]
|
|
217
|
+
console.log(`[ok] Service health: ${data.status ?? 'ok'}`);
|
|
218
|
+
serviceHealthy = true;
|
|
170
219
|
} else {
|
|
171
|
-
console.log(`[!!]
|
|
220
|
+
console.log(`[!!] Service health: HTTP ${res.status}`);
|
|
172
221
|
allOk = false;
|
|
173
222
|
}
|
|
174
|
-
} catch {
|
|
175
|
-
|
|
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.
|
|
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
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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:
|
|
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
|
-
}
|
|
202
|
-
console.log('[
|
|
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
|
-
|
|
10
|
-
|
|
10
|
+
import { loadConfig } from './tui/config.js';
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
function readJson(path) {
|
|
11
13
|
try {
|
|
12
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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]
|
|
207
|
+
console.log(`[ok] Service health: ${data.status ?? 'ok'}`);
|
|
208
|
+
serviceHealthy = true;
|
|
169
209
|
}
|
|
170
210
|
else {
|
|
171
|
-
console.log(`[!!]
|
|
211
|
+
console.log(`[!!] Service health: HTTP ${res.status}`);
|
|
172
212
|
allOk = false;
|
|
173
213
|
}
|
|
174
214
|
}
|
|
175
|
-
catch {
|
|
176
|
-
|
|
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.
|
|
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
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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('[!!]
|
|
299
|
+
console.log('[!!] OpenClaw config: safeclaw not enabled');
|
|
200
300
|
allOk = false;
|
|
201
301
|
}
|
|
202
302
|
}
|
|
203
|
-
|
|
204
|
-
console.log('[
|
|
303
|
+
else {
|
|
304
|
+
console.log('[!!] OpenClaw config: not found');
|
|
305
|
+
allOk = false;
|
|
205
306
|
}
|
|
206
307
|
// Summary
|
|
207
308
|
console.log('');
|
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.
|
|
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;
|