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 CHANGED
@@ -267,7 +267,55 @@ if (command === 'connect') {
267
267
  }
268
268
  }
269
269
 
270
- // 5. OpenClaw installed
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
- // 6. Plugin extension files exist
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
- // 7. Plugin enabled in OpenClaw config
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. OpenClaw installed
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
- // 6. Plugin extension files exist
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
- // 7. Plugin enabled in OpenClaw config
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.2',
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().then(() => sendHeartbeat()).catch(() => { });
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.2',
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().then(() => sendHeartbeat()).catch(() => {});
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.8.2",
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",