openclaw-safeclaw-plugin 0.8.2 → 0.9.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/cli.tsx +51 -3
- package/dist/cli.js +57 -3
- package/dist/index.js +32 -3
- package/index.ts +38 -3
- package/package.json +1 -1
package/cli.tsx
CHANGED
|
@@ -267,7 +267,55 @@ if (command === 'connect') {
|
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
-
// 5.
|
|
270
|
+
// 5. Handshake — validates API key actually works
|
|
271
|
+
if (serviceHealthy && cfg.apiKey) {
|
|
272
|
+
try {
|
|
273
|
+
const res = await fetch(`${cfg.serviceUrl}/handshake`, {
|
|
274
|
+
method: 'POST',
|
|
275
|
+
headers: {
|
|
276
|
+
'Content-Type': 'application/json',
|
|
277
|
+
'Authorization': `Bearer ${cfg.apiKey}`,
|
|
278
|
+
},
|
|
279
|
+
body: JSON.stringify({ pluginVersion: '0.1.3', configHash: '' }),
|
|
280
|
+
signal: AbortSignal.timeout(cfg.timeoutMs),
|
|
281
|
+
});
|
|
282
|
+
if (res.ok) {
|
|
283
|
+
const data = await res.json() as Record<string, unknown>;
|
|
284
|
+
console.log(`[ok] Handshake: org=${data.orgId}, scope=${data.scope}, engine=${data.engineReady ? 'ready' : 'not ready'}`);
|
|
285
|
+
} else {
|
|
286
|
+
let detail = `HTTP ${res.status}`;
|
|
287
|
+
try {
|
|
288
|
+
const body = await res.json() as Record<string, unknown>;
|
|
289
|
+
detail = (body.error ?? body.detail ?? detail) as string;
|
|
290
|
+
} catch { /* ignore parse errors */ }
|
|
291
|
+
console.log(`[!!] Handshake failed: ${detail}`);
|
|
292
|
+
if (res.status === 401) {
|
|
293
|
+
console.log(' ↳ API key is invalid or revoked. Get a new key at https://safeclaw.eu/dashboard');
|
|
294
|
+
} else if (res.status === 403) {
|
|
295
|
+
console.log(' ↳ API key lacks required scope. Check key permissions in your dashboard.');
|
|
296
|
+
} else if (res.status === 500) {
|
|
297
|
+
console.log(' ↳ Server error — check service logs for details.');
|
|
298
|
+
}
|
|
299
|
+
allOk = false;
|
|
300
|
+
}
|
|
301
|
+
} catch (e) {
|
|
302
|
+
const isTimeout = e instanceof DOMException && e.name === 'TimeoutError';
|
|
303
|
+
if (isTimeout) {
|
|
304
|
+
console.log(`[!!] Handshake failed: timeout after ${cfg.timeoutMs}ms`);
|
|
305
|
+
console.log(' ↳ Service may be overloaded. Try increasing SAFECLAW_TIMEOUT_MS.');
|
|
306
|
+
} else {
|
|
307
|
+
console.log('[!!] Handshake failed: could not connect');
|
|
308
|
+
console.log(` ↳ Is the service running at ${cfg.serviceUrl}?`);
|
|
309
|
+
}
|
|
310
|
+
allOk = false;
|
|
311
|
+
}
|
|
312
|
+
} else if (serviceHealthy && !cfg.apiKey) {
|
|
313
|
+
console.log('[!!] Handshake: skipped — no API key configured');
|
|
314
|
+
console.log(' ↳ Run: safeclaw connect <your-api-key>');
|
|
315
|
+
allOk = false;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// 6. OpenClaw installed
|
|
271
319
|
try {
|
|
272
320
|
execSync('which openclaw', { encoding: 'utf-8', stdio: 'pipe' });
|
|
273
321
|
console.log('[ok] OpenClaw: installed');
|
|
@@ -276,7 +324,7 @@ if (command === 'connect') {
|
|
|
276
324
|
allOk = false;
|
|
277
325
|
}
|
|
278
326
|
|
|
279
|
-
//
|
|
327
|
+
// 7. Plugin extension files exist
|
|
280
328
|
const extensionDir = join(homedir(), '.openclaw', 'extensions', 'safeclaw');
|
|
281
329
|
const hasManifest = existsSync(join(extensionDir, 'openclaw.plugin.json'));
|
|
282
330
|
const hasEntry = existsSync(join(extensionDir, 'index.js'));
|
|
@@ -295,7 +343,7 @@ if (command === 'connect') {
|
|
|
295
343
|
allOk = false;
|
|
296
344
|
}
|
|
297
345
|
|
|
298
|
-
//
|
|
346
|
+
// 8. Plugin enabled in OpenClaw config
|
|
299
347
|
const ocConfigPath = join(homedir(), '.openclaw', 'openclaw.json');
|
|
300
348
|
if (existsSync(ocConfigPath)) {
|
|
301
349
|
const ocConfig = readJson(ocConfigPath);
|
package/dist/cli.js
CHANGED
|
@@ -261,7 +261,61 @@ else if (command === 'status') {
|
|
|
261
261
|
allOk = false;
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
|
-
// 5.
|
|
264
|
+
// 5. Handshake — validates API key actually works
|
|
265
|
+
if (serviceHealthy && cfg.apiKey) {
|
|
266
|
+
try {
|
|
267
|
+
const res = await fetch(`${cfg.serviceUrl}/handshake`, {
|
|
268
|
+
method: 'POST',
|
|
269
|
+
headers: {
|
|
270
|
+
'Content-Type': 'application/json',
|
|
271
|
+
'Authorization': `Bearer ${cfg.apiKey}`,
|
|
272
|
+
},
|
|
273
|
+
body: JSON.stringify({ pluginVersion: '0.1.3', configHash: '' }),
|
|
274
|
+
signal: AbortSignal.timeout(cfg.timeoutMs),
|
|
275
|
+
});
|
|
276
|
+
if (res.ok) {
|
|
277
|
+
const data = await res.json();
|
|
278
|
+
console.log(`[ok] Handshake: org=${data.orgId}, scope=${data.scope}, engine=${data.engineReady ? 'ready' : 'not ready'}`);
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
let detail = `HTTP ${res.status}`;
|
|
282
|
+
try {
|
|
283
|
+
const body = await res.json();
|
|
284
|
+
detail = (body.error ?? body.detail ?? detail);
|
|
285
|
+
}
|
|
286
|
+
catch { /* ignore parse errors */ }
|
|
287
|
+
console.log(`[!!] Handshake failed: ${detail}`);
|
|
288
|
+
if (res.status === 401) {
|
|
289
|
+
console.log(' ↳ API key is invalid or revoked. Get a new key at https://safeclaw.eu/dashboard');
|
|
290
|
+
}
|
|
291
|
+
else if (res.status === 403) {
|
|
292
|
+
console.log(' ↳ API key lacks required scope. Check key permissions in your dashboard.');
|
|
293
|
+
}
|
|
294
|
+
else if (res.status === 500) {
|
|
295
|
+
console.log(' ↳ Server error — check service logs for details.');
|
|
296
|
+
}
|
|
297
|
+
allOk = false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch (e) {
|
|
301
|
+
const isTimeout = e instanceof DOMException && e.name === 'TimeoutError';
|
|
302
|
+
if (isTimeout) {
|
|
303
|
+
console.log(`[!!] Handshake failed: timeout after ${cfg.timeoutMs}ms`);
|
|
304
|
+
console.log(' ↳ Service may be overloaded. Try increasing SAFECLAW_TIMEOUT_MS.');
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
console.log('[!!] Handshake failed: could not connect');
|
|
308
|
+
console.log(` ↳ Is the service running at ${cfg.serviceUrl}?`);
|
|
309
|
+
}
|
|
310
|
+
allOk = false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
else if (serviceHealthy && !cfg.apiKey) {
|
|
314
|
+
console.log('[!!] Handshake: skipped — no API key configured');
|
|
315
|
+
console.log(' ↳ Run: safeclaw connect <your-api-key>');
|
|
316
|
+
allOk = false;
|
|
317
|
+
}
|
|
318
|
+
// 6. OpenClaw installed
|
|
265
319
|
try {
|
|
266
320
|
execSync('which openclaw', { encoding: 'utf-8', stdio: 'pipe' });
|
|
267
321
|
console.log('[ok] OpenClaw: installed');
|
|
@@ -270,7 +324,7 @@ else if (command === 'status') {
|
|
|
270
324
|
console.log('[!!] OpenClaw: not found in PATH');
|
|
271
325
|
allOk = false;
|
|
272
326
|
}
|
|
273
|
-
//
|
|
327
|
+
// 7. Plugin extension files exist
|
|
274
328
|
const extensionDir = join(homedir(), '.openclaw', 'extensions', 'safeclaw');
|
|
275
329
|
const hasManifest = existsSync(join(extensionDir, 'openclaw.plugin.json'));
|
|
276
330
|
const hasEntry = existsSync(join(extensionDir, 'index.js'));
|
|
@@ -291,7 +345,7 @@ else if (command === 'status') {
|
|
|
291
345
|
console.log('[!!] Plugin: not installed. Run: safeclaw setup');
|
|
292
346
|
allOk = false;
|
|
293
347
|
}
|
|
294
|
-
//
|
|
348
|
+
// 8. Plugin enabled in OpenClaw config
|
|
295
349
|
const ocConfigPath = join(homedir(), '.openclaw', 'openclaw.json');
|
|
296
350
|
if (existsSync(ocConfigPath)) {
|
|
297
351
|
const ocConfig = readJson(ocConfigPath);
|
package/dist/index.js
CHANGED
|
@@ -53,6 +53,24 @@ async function post(path, body) {
|
|
|
53
53
|
return null; // Caller checks failMode
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
+
let handshakeCompleted = false;
|
|
57
|
+
async function performHandshake() {
|
|
58
|
+
if (!config.apiKey) {
|
|
59
|
+
console.warn('[SafeClaw] No API key configured — skipping handshake');
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const r = await post('/handshake', {
|
|
63
|
+
pluginVersion: '0.1.3',
|
|
64
|
+
configHash: configHash(config),
|
|
65
|
+
});
|
|
66
|
+
if (r === null) {
|
|
67
|
+
console.warn('[SafeClaw] ✗ Handshake failed — API key may be invalid or service unreachable');
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
console.log(`[SafeClaw] ✓ Handshake OK — org=${r.orgId}, scope=${r.scope}, engine=${r.engineReady ? 'ready' : 'not ready'}`);
|
|
71
|
+
handshakeCompleted = true;
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
56
74
|
async function checkConnection() {
|
|
57
75
|
const label = `[SafeClaw]`;
|
|
58
76
|
console.log(`${label} Connecting to ${config.serviceUrl} ...`);
|
|
@@ -82,7 +100,7 @@ async function checkConnection() {
|
|
|
82
100
|
export default {
|
|
83
101
|
id: 'safeclaw',
|
|
84
102
|
name: 'SafeClaw Neurosymbolic Governance',
|
|
85
|
-
version: '0.1.
|
|
103
|
+
version: '0.1.3',
|
|
86
104
|
register(api) {
|
|
87
105
|
if (!config.enabled) {
|
|
88
106
|
console.log('[SafeClaw] Plugin disabled');
|
|
@@ -106,8 +124,16 @@ export default {
|
|
|
106
124
|
// Heartbeat failure is non-fatal
|
|
107
125
|
}
|
|
108
126
|
};
|
|
109
|
-
// Start heartbeat after connection check
|
|
110
|
-
checkConnection()
|
|
127
|
+
// Start heartbeat after connection check + handshake
|
|
128
|
+
checkConnection()
|
|
129
|
+
.then(() => performHandshake())
|
|
130
|
+
.then((ok) => {
|
|
131
|
+
if (!ok && config.failMode === 'closed') {
|
|
132
|
+
console.warn('[SafeClaw] ⚠ Handshake failed with fail-mode=closed — tool calls will be BLOCKED');
|
|
133
|
+
}
|
|
134
|
+
return sendHeartbeat();
|
|
135
|
+
})
|
|
136
|
+
.catch(() => { });
|
|
111
137
|
const heartbeatInterval = setInterval(sendHeartbeat, 30000);
|
|
112
138
|
// Clean shutdown: send shutdown heartbeat and clear interval
|
|
113
139
|
const shutdown = () => {
|
|
@@ -127,6 +153,9 @@ export default {
|
|
|
127
153
|
process.on('SIGTERM', () => { shutdown(); process.exit(0); });
|
|
128
154
|
// THE GATE — constraint checking on every tool call
|
|
129
155
|
api.on('before_tool_call', async (event, ctx) => {
|
|
156
|
+
if (!handshakeCompleted && config.failMode === 'closed' && config.enforcement === 'enforce') {
|
|
157
|
+
return { block: true, blockReason: 'SafeClaw handshake not completed (fail-closed)' };
|
|
158
|
+
}
|
|
130
159
|
const r = await post('/evaluate/tool-call', {
|
|
131
160
|
sessionId: ctx.sessionId ?? event.sessionId,
|
|
132
161
|
userId: ctx.userId ?? event.userId,
|
package/index.ts
CHANGED
|
@@ -79,6 +79,29 @@ interface PluginApi {
|
|
|
79
79
|
): void;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
let handshakeCompleted = false;
|
|
83
|
+
|
|
84
|
+
async function performHandshake(): Promise<boolean> {
|
|
85
|
+
if (!config.apiKey) {
|
|
86
|
+
console.warn('[SafeClaw] No API key configured — skipping handshake');
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const r = await post('/handshake', {
|
|
91
|
+
pluginVersion: '0.1.3',
|
|
92
|
+
configHash: configHash(config),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (r === null) {
|
|
96
|
+
console.warn('[SafeClaw] ✗ Handshake failed — API key may be invalid or service unreachable');
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log(`[SafeClaw] ✓ Handshake OK — org=${r.orgId}, scope=${r.scope}, engine=${r.engineReady ? 'ready' : 'not ready'}`);
|
|
101
|
+
handshakeCompleted = true;
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
82
105
|
async function checkConnection(): Promise<void> {
|
|
83
106
|
const label = `[SafeClaw]`;
|
|
84
107
|
console.log(`${label} Connecting to ${config.serviceUrl} ...`);
|
|
@@ -107,7 +130,7 @@ async function checkConnection(): Promise<void> {
|
|
|
107
130
|
export default {
|
|
108
131
|
id: 'safeclaw',
|
|
109
132
|
name: 'SafeClaw Neurosymbolic Governance',
|
|
110
|
-
version: '0.1.
|
|
133
|
+
version: '0.1.3',
|
|
111
134
|
|
|
112
135
|
register(api: PluginApi) {
|
|
113
136
|
if (!config.enabled) {
|
|
@@ -133,8 +156,16 @@ export default {
|
|
|
133
156
|
}
|
|
134
157
|
};
|
|
135
158
|
|
|
136
|
-
// Start heartbeat after connection check
|
|
137
|
-
checkConnection()
|
|
159
|
+
// Start heartbeat after connection check + handshake
|
|
160
|
+
checkConnection()
|
|
161
|
+
.then(() => performHandshake())
|
|
162
|
+
.then((ok) => {
|
|
163
|
+
if (!ok && config.failMode === 'closed') {
|
|
164
|
+
console.warn('[SafeClaw] ⚠ Handshake failed with fail-mode=closed — tool calls will be BLOCKED');
|
|
165
|
+
}
|
|
166
|
+
return sendHeartbeat();
|
|
167
|
+
})
|
|
168
|
+
.catch(() => {});
|
|
138
169
|
const heartbeatInterval = setInterval(sendHeartbeat, 30000);
|
|
139
170
|
|
|
140
171
|
// Clean shutdown: send shutdown heartbeat and clear interval
|
|
@@ -156,6 +187,10 @@ export default {
|
|
|
156
187
|
|
|
157
188
|
// THE GATE — constraint checking on every tool call
|
|
158
189
|
api.on('before_tool_call', async (event: PluginEvent, ctx: PluginContext) => {
|
|
190
|
+
if (!handshakeCompleted && config.failMode === 'closed' && config.enforcement === 'enforce') {
|
|
191
|
+
return { block: true, blockReason: 'SafeClaw handshake not completed (fail-closed)' };
|
|
192
|
+
}
|
|
193
|
+
|
|
159
194
|
const r = await post('/evaluate/tool-call', {
|
|
160
195
|
sessionId: ctx.sessionId ?? event.sessionId,
|
|
161
196
|
userId: ctx.userId ?? event.userId,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-safeclaw-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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",
|