openclaw-safeclaw-plugin 1.4.0 → 1.5.0
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/dist/index.js +51 -18
- package/dist/tui/config.js +6 -1
- package/index.ts +54 -19
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/tui/config.ts +3 -1
- package/types/openclaw-sdk.d.ts +36 -1
package/dist/index.js
CHANGED
|
@@ -97,8 +97,19 @@ async function get(path) {
|
|
|
97
97
|
return null;
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
|
+
function approvalSeverityForRisk(riskLevel) {
|
|
101
|
+
if (riskLevel === 'CriticalRisk' || riskLevel === 'HighRisk')
|
|
102
|
+
return 'critical';
|
|
103
|
+
if (riskLevel === 'MediumRisk')
|
|
104
|
+
return 'warning';
|
|
105
|
+
return 'info';
|
|
106
|
+
}
|
|
107
|
+
function approvalTimeoutBehaviorForRisk(riskLevel) {
|
|
108
|
+
return riskLevel === 'CriticalRisk' || riskLevel === 'HighRisk' ? 'deny' : 'allow';
|
|
109
|
+
}
|
|
100
110
|
// --- Plugin Definition ---
|
|
101
111
|
let handshakeCompleted = false;
|
|
112
|
+
let lastHandshakeConfigHash = '';
|
|
102
113
|
async function performHandshake() {
|
|
103
114
|
const cfg = getConfig();
|
|
104
115
|
if (!cfg.apiKey) {
|
|
@@ -115,6 +126,7 @@ async function performHandshake() {
|
|
|
115
126
|
}
|
|
116
127
|
log.info(`[SafeClaw] Handshake OK — org=${r.orgId}, scope=${r.scope}, engine=${r.engineReady ? 'ready' : 'not ready'}`);
|
|
117
128
|
handshakeCompleted = true;
|
|
129
|
+
lastHandshakeConfigHash = configHash(getConfig());
|
|
118
130
|
return true;
|
|
119
131
|
}
|
|
120
132
|
async function checkConnection() {
|
|
@@ -160,9 +172,15 @@ export default {
|
|
|
160
172
|
// Heartbeat watchdog — send config hash to service every 30s
|
|
161
173
|
const sendHeartbeat = async () => {
|
|
162
174
|
try {
|
|
175
|
+
const currentHash = configHash(getConfig());
|
|
176
|
+
if (handshakeCompleted && currentHash !== lastHandshakeConfigHash) {
|
|
177
|
+
log.info('[SafeClaw] Config changed — re-authenticating');
|
|
178
|
+
handshakeCompleted = false;
|
|
179
|
+
performHandshake().catch(() => { });
|
|
180
|
+
}
|
|
163
181
|
await post('/heartbeat', {
|
|
164
182
|
agentId: instanceId,
|
|
165
|
-
configHash:
|
|
183
|
+
configHash: currentHash,
|
|
166
184
|
status: 'alive',
|
|
167
185
|
});
|
|
168
186
|
}
|
|
@@ -237,6 +255,19 @@ export default {
|
|
|
237
255
|
else if (r === null && cfg.failMode === 'closed' && cfg.enforcement === 'audit-only') {
|
|
238
256
|
log.warn(`[SafeClaw] Service unavailable at ${cfg.serviceUrl} (fail-closed mode, audit-only)`);
|
|
239
257
|
}
|
|
258
|
+
// If service says confirmation required, use OpenClaw's native approval flow
|
|
259
|
+
if (r?.confirmationRequired) {
|
|
260
|
+
const riskLevel = r.riskLevel || '';
|
|
261
|
+
return {
|
|
262
|
+
requireApproval: {
|
|
263
|
+
title: 'SafeClaw Governance Check',
|
|
264
|
+
description: r.reason || 'This action requires confirmation',
|
|
265
|
+
severity: approvalSeverityForRisk(riskLevel),
|
|
266
|
+
timeoutMs: 30_000,
|
|
267
|
+
timeoutBehavior: approvalTimeoutBehaviorForRisk(riskLevel),
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
240
271
|
if (r?.block) {
|
|
241
272
|
const blockReason = r.reason || 'Blocked by SafeClaw (no reason provided)';
|
|
242
273
|
if (cfg.enforcement === 'enforce') {
|
|
@@ -300,7 +331,7 @@ export default {
|
|
|
300
331
|
content: event.prompt ?? '',
|
|
301
332
|
provider: event.provider ?? '',
|
|
302
333
|
model: event.model ?? '',
|
|
303
|
-
}).catch(() =>
|
|
334
|
+
}).catch((e) => log.warn('[SafeClaw] Failed to log LLM input:', e));
|
|
304
335
|
});
|
|
305
336
|
api.on('llm_output', (event, ctx) => {
|
|
306
337
|
post('/log/llm-output', {
|
|
@@ -309,33 +340,35 @@ export default {
|
|
|
309
340
|
provider: event.provider ?? '',
|
|
310
341
|
model: event.model ?? '',
|
|
311
342
|
usage: event.usage ?? {},
|
|
312
|
-
}).catch(() =>
|
|
343
|
+
}).catch((e) => log.warn('[SafeClaw] Failed to log LLM output:', e));
|
|
313
344
|
});
|
|
314
345
|
// (#195: use event.toolName, !event.error for success, add durationMs and error)
|
|
315
346
|
api.on('after_tool_call', (event, ctx) => {
|
|
316
347
|
post('/record/tool-result', {
|
|
317
348
|
sessionId: ctx.sessionId ?? event.sessionId ?? '',
|
|
349
|
+
userId: ctx.agentId ?? '',
|
|
318
350
|
toolName: event.toolName ?? '',
|
|
319
351
|
params: event.params ?? {},
|
|
320
352
|
result: event.result ?? '',
|
|
321
353
|
success: !event.error,
|
|
322
354
|
error: event.error ? String(event.error) : '',
|
|
323
355
|
durationMs: event.durationMs ?? 0,
|
|
356
|
+
runId: ctx.runId ?? event.runId ?? '',
|
|
324
357
|
}).catch((e) => log.warn('[SafeClaw] Failed to record tool result:', e));
|
|
325
358
|
});
|
|
326
359
|
// Subagent governance — block delegation bypass attempts (#188)
|
|
327
360
|
api.on('subagent_spawning', async (event, ctx) => {
|
|
328
361
|
const cfg = getConfig();
|
|
329
362
|
const r = await post('/evaluate/subagent-spawn', {
|
|
330
|
-
sessionId: ctx.sessionId ?? event.sessionId,
|
|
331
|
-
userId: ctx.agentId,
|
|
363
|
+
sessionId: ctx.sessionId ?? event.sessionId ?? '',
|
|
364
|
+
userId: ctx.agentId ?? '',
|
|
332
365
|
parentAgentId: event.parentAgentId,
|
|
333
366
|
childConfig: event.childConfig ?? {},
|
|
334
367
|
reason: event.reason ?? '',
|
|
335
368
|
});
|
|
336
369
|
// Fail-closed handling (matches before_tool_call / message_sending pattern)
|
|
337
370
|
if (r === null && cfg.failMode === 'closed' && cfg.enforcement === 'enforce') {
|
|
338
|
-
|
|
371
|
+
return { status: 'error', error: 'SafeClaw service unavailable (fail-closed mode)' };
|
|
339
372
|
}
|
|
340
373
|
else if (r === null && cfg.failMode === 'closed' && cfg.enforcement === 'warn-only') {
|
|
341
374
|
log.warn('[SafeClaw] Service unavailable (fail-closed mode, warn-only)');
|
|
@@ -344,7 +377,7 @@ export default {
|
|
|
344
377
|
log.warn('[SafeClaw] Service unavailable (fail-closed mode, audit-only)');
|
|
345
378
|
}
|
|
346
379
|
if (r?.block && cfg.enforcement === 'enforce') {
|
|
347
|
-
|
|
380
|
+
return { status: 'error', error: r.reason || 'Blocked by SafeClaw: delegation bypass detected' };
|
|
348
381
|
}
|
|
349
382
|
if (r?.block && cfg.enforcement === 'warn-only') {
|
|
350
383
|
log.warn(`[SafeClaw] Subagent spawn warning: ${r.reason}`);
|
|
@@ -353,38 +386,38 @@ export default {
|
|
|
353
386
|
// Subagent ended — record child agent lifecycle (#188)
|
|
354
387
|
api.on('subagent_ended', (event, ctx) => {
|
|
355
388
|
post('/record/subagent-ended', {
|
|
356
|
-
sessionId: ctx.sessionId ?? event.sessionId,
|
|
389
|
+
sessionId: ctx.sessionId ?? event.sessionId ?? '',
|
|
357
390
|
parentAgentId: event.parentAgentId,
|
|
358
391
|
childAgentId: event.childAgentId,
|
|
359
|
-
}).catch(() =>
|
|
392
|
+
}).catch((e) => log.warn('[SafeClaw] Failed to record subagent ended:', e));
|
|
360
393
|
});
|
|
361
394
|
// Session lifecycle — notify service of session start (#189)
|
|
362
395
|
api.on('session_start', (event, ctx) => {
|
|
363
396
|
post('/session/start', {
|
|
364
|
-
sessionId: ctx.sessionId ?? event.sessionId,
|
|
365
|
-
userId: ctx.agentId,
|
|
397
|
+
sessionId: ctx.sessionId ?? event.sessionId ?? '',
|
|
398
|
+
userId: ctx.agentId ?? '',
|
|
366
399
|
agentId: instanceId,
|
|
367
400
|
metadata: event.metadata ?? {},
|
|
368
|
-
}).catch(() =>
|
|
401
|
+
}).catch((e) => log.warn('[SafeClaw] Failed to record session start:', e));
|
|
369
402
|
});
|
|
370
403
|
// Session lifecycle — notify service of session end (#189)
|
|
371
404
|
api.on('session_end', (event, ctx) => {
|
|
372
405
|
post('/session/end', {
|
|
373
|
-
sessionId: ctx.sessionId ?? event.sessionId,
|
|
374
|
-
userId: ctx.agentId,
|
|
406
|
+
sessionId: ctx.sessionId ?? event.sessionId ?? '',
|
|
407
|
+
userId: ctx.agentId ?? '',
|
|
375
408
|
agentId: instanceId,
|
|
376
|
-
}).catch(() =>
|
|
409
|
+
}).catch((e) => log.warn('[SafeClaw] Failed to record session end:', e));
|
|
377
410
|
});
|
|
378
411
|
// Inbound message governance — evaluate received messages (#190)
|
|
379
412
|
api.on('message_received', (event, ctx) => {
|
|
380
413
|
post('/evaluate/inbound-message', {
|
|
381
|
-
sessionId: ctx.sessionId ?? event.sessionId,
|
|
382
|
-
userId: ctx.agentId,
|
|
414
|
+
sessionId: ctx.sessionId ?? event.sessionId ?? '',
|
|
415
|
+
userId: ctx.agentId ?? '',
|
|
383
416
|
channel: event.channel ?? ctx.channelId ?? '',
|
|
384
417
|
sender: event.sender ?? '',
|
|
385
418
|
content: event.content ?? '',
|
|
386
419
|
metadata: event.metadata ?? {},
|
|
387
|
-
}).catch(() =>
|
|
420
|
+
}).catch((e) => log.warn('[SafeClaw] Failed to evaluate inbound message:', e));
|
|
388
421
|
});
|
|
389
422
|
// Agent tools — let agents introspect governance state (#197)
|
|
390
423
|
if (api.registerTool) {
|
package/dist/tui/config.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Reads ~/.safeclaw/config.json, applies env-var overrides,
|
|
6
6
|
* and exposes helpers for saving and hashing config state.
|
|
7
7
|
*/
|
|
8
|
-
import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
8
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync, chmodSync } from 'fs';
|
|
9
9
|
import { join, dirname } from 'path';
|
|
10
10
|
import { homedir } from 'os';
|
|
11
11
|
import crypto from 'crypto';
|
|
@@ -167,6 +167,10 @@ export function saveConfig(config) {
|
|
|
167
167
|
mkdirSync(dirname(CONFIG_PATH), { recursive: true, mode: 0o700 });
|
|
168
168
|
try {
|
|
169
169
|
writeFileSync(CONFIG_PATH, JSON.stringify(existing, null, 2) + '\n', { encoding: 'utf-8', mode: 0o600 });
|
|
170
|
+
try {
|
|
171
|
+
chmodSync(CONFIG_PATH, 0o600);
|
|
172
|
+
}
|
|
173
|
+
catch { /* best-effort */ }
|
|
170
174
|
}
|
|
171
175
|
catch (e) {
|
|
172
176
|
const code = e.code;
|
|
@@ -189,6 +193,7 @@ export function configHash(config) {
|
|
|
189
193
|
enforcement: config.enforcement,
|
|
190
194
|
failMode: config.failMode,
|
|
191
195
|
serviceUrl: config.serviceUrl,
|
|
196
|
+
apiKeyFingerprint: config.apiKey ? config.apiKey.slice(0, 4) + config.apiKey.slice(-4) : '',
|
|
192
197
|
});
|
|
193
198
|
return crypto.createHash('sha256').update(payload).digest('hex');
|
|
194
199
|
}
|
package/index.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* to the SafeClaw service and acts on the responses.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type { OpenClawPluginApi, OpenClawPluginEvent, OpenClawPluginContext } from 'openclaw/plugin-sdk/core';
|
|
10
|
+
import type { OpenClawPluginApi, OpenClawPluginEvent, OpenClawPluginContext, BeforeToolCallResult } from 'openclaw/plugin-sdk/core';
|
|
11
11
|
import { loadConfig, configHash } from './tui/config.js';
|
|
12
12
|
import crypto from 'crypto';
|
|
13
13
|
import { createRequire } from 'module';
|
|
@@ -104,9 +104,20 @@ async function get(path: string): Promise<Record<string, unknown> | null> {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
function approvalSeverityForRisk(riskLevel: string): 'info' | 'warning' | 'critical' {
|
|
108
|
+
if (riskLevel === 'CriticalRisk' || riskLevel === 'HighRisk') return 'critical';
|
|
109
|
+
if (riskLevel === 'MediumRisk') return 'warning';
|
|
110
|
+
return 'info';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function approvalTimeoutBehaviorForRisk(riskLevel: string): 'allow' | 'deny' {
|
|
114
|
+
return riskLevel === 'CriticalRisk' || riskLevel === 'HighRisk' ? 'deny' : 'allow';
|
|
115
|
+
}
|
|
116
|
+
|
|
107
117
|
// --- Plugin Definition ---
|
|
108
118
|
|
|
109
119
|
let handshakeCompleted = false;
|
|
120
|
+
let lastHandshakeConfigHash = '';
|
|
110
121
|
|
|
111
122
|
async function performHandshake(): Promise<boolean> {
|
|
112
123
|
const cfg = getConfig();
|
|
@@ -127,6 +138,7 @@ async function performHandshake(): Promise<boolean> {
|
|
|
127
138
|
|
|
128
139
|
log.info(`[SafeClaw] Handshake OK — org=${r.orgId}, scope=${r.scope}, engine=${r.engineReady ? 'ready' : 'not ready'}`);
|
|
129
140
|
handshakeCompleted = true;
|
|
141
|
+
lastHandshakeConfigHash = configHash(getConfig());
|
|
130
142
|
return true;
|
|
131
143
|
}
|
|
132
144
|
|
|
@@ -176,9 +188,15 @@ export default {
|
|
|
176
188
|
// Heartbeat watchdog — send config hash to service every 30s
|
|
177
189
|
const sendHeartbeat = async () => {
|
|
178
190
|
try {
|
|
191
|
+
const currentHash = configHash(getConfig());
|
|
192
|
+
if (handshakeCompleted && currentHash !== lastHandshakeConfigHash) {
|
|
193
|
+
log.info('[SafeClaw] Config changed — re-authenticating');
|
|
194
|
+
handshakeCompleted = false;
|
|
195
|
+
performHandshake().catch(() => {});
|
|
196
|
+
}
|
|
179
197
|
await post('/heartbeat', {
|
|
180
198
|
agentId: instanceId,
|
|
181
|
-
configHash:
|
|
199
|
+
configHash: currentHash,
|
|
182
200
|
status: 'alive',
|
|
183
201
|
});
|
|
184
202
|
} catch {
|
|
@@ -254,6 +272,21 @@ export default {
|
|
|
254
272
|
} else if (r === null && cfg.failMode === 'closed' && cfg.enforcement === 'audit-only') {
|
|
255
273
|
log.warn(`[SafeClaw] Service unavailable at ${cfg.serviceUrl} (fail-closed mode, audit-only)`);
|
|
256
274
|
}
|
|
275
|
+
|
|
276
|
+
// If service says confirmation required, use OpenClaw's native approval flow
|
|
277
|
+
if (r?.confirmationRequired) {
|
|
278
|
+
const riskLevel = (r.riskLevel as string) || '';
|
|
279
|
+
return {
|
|
280
|
+
requireApproval: {
|
|
281
|
+
title: 'SafeClaw Governance Check',
|
|
282
|
+
description: (r.reason as string) || 'This action requires confirmation',
|
|
283
|
+
severity: approvalSeverityForRisk(riskLevel),
|
|
284
|
+
timeoutMs: 30_000,
|
|
285
|
+
timeoutBehavior: approvalTimeoutBehaviorForRisk(riskLevel),
|
|
286
|
+
},
|
|
287
|
+
} satisfies BeforeToolCallResult;
|
|
288
|
+
}
|
|
289
|
+
|
|
257
290
|
if (r?.block) {
|
|
258
291
|
const blockReason = (r.reason as string) || 'Blocked by SafeClaw (no reason provided)';
|
|
259
292
|
if (cfg.enforcement === 'enforce') {
|
|
@@ -320,7 +353,7 @@ export default {
|
|
|
320
353
|
content: event.prompt ?? '',
|
|
321
354
|
provider: event.provider ?? '',
|
|
322
355
|
model: event.model ?? '',
|
|
323
|
-
}).catch(() =>
|
|
356
|
+
}).catch((e) => log.warn('[SafeClaw] Failed to log LLM input:', e));
|
|
324
357
|
});
|
|
325
358
|
|
|
326
359
|
api.on('llm_output', (event: OpenClawPluginEvent, ctx: OpenClawPluginContext) => {
|
|
@@ -330,19 +363,21 @@ export default {
|
|
|
330
363
|
provider: event.provider ?? '',
|
|
331
364
|
model: event.model ?? '',
|
|
332
365
|
usage: event.usage ?? {},
|
|
333
|
-
}).catch(() =>
|
|
366
|
+
}).catch((e) => log.warn('[SafeClaw] Failed to log LLM output:', e));
|
|
334
367
|
});
|
|
335
368
|
|
|
336
369
|
// (#195: use event.toolName, !event.error for success, add durationMs and error)
|
|
337
370
|
api.on('after_tool_call', (event: OpenClawPluginEvent, ctx: OpenClawPluginContext) => {
|
|
338
371
|
post('/record/tool-result', {
|
|
339
372
|
sessionId: ctx.sessionId ?? event.sessionId ?? '',
|
|
373
|
+
userId: ctx.agentId ?? '',
|
|
340
374
|
toolName: event.toolName ?? '',
|
|
341
375
|
params: event.params ?? {},
|
|
342
376
|
result: event.result ?? '',
|
|
343
377
|
success: !event.error,
|
|
344
378
|
error: event.error ? String(event.error) : '',
|
|
345
379
|
durationMs: event.durationMs ?? 0,
|
|
380
|
+
runId: ctx.runId ?? event.runId ?? '',
|
|
346
381
|
}).catch((e) => log.warn('[SafeClaw] Failed to record tool result:', e));
|
|
347
382
|
});
|
|
348
383
|
|
|
@@ -350,8 +385,8 @@ export default {
|
|
|
350
385
|
api.on('subagent_spawning', async (event: OpenClawPluginEvent, ctx: OpenClawPluginContext) => {
|
|
351
386
|
const cfg = getConfig();
|
|
352
387
|
const r = await post('/evaluate/subagent-spawn', {
|
|
353
|
-
sessionId: ctx.sessionId ?? event.sessionId,
|
|
354
|
-
userId: ctx.agentId,
|
|
388
|
+
sessionId: ctx.sessionId ?? event.sessionId ?? '',
|
|
389
|
+
userId: ctx.agentId ?? '',
|
|
355
390
|
parentAgentId: event.parentAgentId,
|
|
356
391
|
childConfig: event.childConfig ?? {},
|
|
357
392
|
reason: event.reason ?? '',
|
|
@@ -359,7 +394,7 @@ export default {
|
|
|
359
394
|
|
|
360
395
|
// Fail-closed handling (matches before_tool_call / message_sending pattern)
|
|
361
396
|
if (r === null && cfg.failMode === 'closed' && cfg.enforcement === 'enforce') {
|
|
362
|
-
|
|
397
|
+
return { status: 'error', error: 'SafeClaw service unavailable (fail-closed mode)' };
|
|
363
398
|
} else if (r === null && cfg.failMode === 'closed' && cfg.enforcement === 'warn-only') {
|
|
364
399
|
log.warn('[SafeClaw] Service unavailable (fail-closed mode, warn-only)');
|
|
365
400
|
} else if (r === null && cfg.failMode === 'closed' && cfg.enforcement === 'audit-only') {
|
|
@@ -367,7 +402,7 @@ export default {
|
|
|
367
402
|
}
|
|
368
403
|
|
|
369
404
|
if (r?.block && cfg.enforcement === 'enforce') {
|
|
370
|
-
|
|
405
|
+
return { status: 'error', error: (r.reason as string) || 'Blocked by SafeClaw: delegation bypass detected' };
|
|
371
406
|
}
|
|
372
407
|
if (r?.block && cfg.enforcement === 'warn-only') {
|
|
373
408
|
log.warn(`[SafeClaw] Subagent spawn warning: ${r.reason}`);
|
|
@@ -377,41 +412,41 @@ export default {
|
|
|
377
412
|
// Subagent ended — record child agent lifecycle (#188)
|
|
378
413
|
api.on('subagent_ended', (event: OpenClawPluginEvent, ctx: OpenClawPluginContext) => {
|
|
379
414
|
post('/record/subagent-ended', {
|
|
380
|
-
sessionId: ctx.sessionId ?? event.sessionId,
|
|
415
|
+
sessionId: ctx.sessionId ?? event.sessionId ?? '',
|
|
381
416
|
parentAgentId: event.parentAgentId,
|
|
382
417
|
childAgentId: event.childAgentId,
|
|
383
|
-
}).catch(() =>
|
|
418
|
+
}).catch((e) => log.warn('[SafeClaw] Failed to record subagent ended:', e));
|
|
384
419
|
});
|
|
385
420
|
|
|
386
421
|
// Session lifecycle — notify service of session start (#189)
|
|
387
422
|
api.on('session_start', (event: OpenClawPluginEvent, ctx: OpenClawPluginContext) => {
|
|
388
423
|
post('/session/start', {
|
|
389
|
-
sessionId: ctx.sessionId ?? event.sessionId,
|
|
390
|
-
userId: ctx.agentId,
|
|
424
|
+
sessionId: ctx.sessionId ?? event.sessionId ?? '',
|
|
425
|
+
userId: ctx.agentId ?? '',
|
|
391
426
|
agentId: instanceId,
|
|
392
427
|
metadata: event.metadata ?? {},
|
|
393
|
-
}).catch(() =>
|
|
428
|
+
}).catch((e) => log.warn('[SafeClaw] Failed to record session start:', e));
|
|
394
429
|
});
|
|
395
430
|
|
|
396
431
|
// Session lifecycle — notify service of session end (#189)
|
|
397
432
|
api.on('session_end', (event: OpenClawPluginEvent, ctx: OpenClawPluginContext) => {
|
|
398
433
|
post('/session/end', {
|
|
399
|
-
sessionId: ctx.sessionId ?? event.sessionId,
|
|
400
|
-
userId: ctx.agentId,
|
|
434
|
+
sessionId: ctx.sessionId ?? event.sessionId ?? '',
|
|
435
|
+
userId: ctx.agentId ?? '',
|
|
401
436
|
agentId: instanceId,
|
|
402
|
-
}).catch(() =>
|
|
437
|
+
}).catch((e) => log.warn('[SafeClaw] Failed to record session end:', e));
|
|
403
438
|
});
|
|
404
439
|
|
|
405
440
|
// Inbound message governance — evaluate received messages (#190)
|
|
406
441
|
api.on('message_received', (event: OpenClawPluginEvent, ctx: OpenClawPluginContext) => {
|
|
407
442
|
post('/evaluate/inbound-message', {
|
|
408
|
-
sessionId: ctx.sessionId ?? event.sessionId,
|
|
409
|
-
userId: ctx.agentId,
|
|
443
|
+
sessionId: ctx.sessionId ?? event.sessionId ?? '',
|
|
444
|
+
userId: ctx.agentId ?? '',
|
|
410
445
|
channel: event.channel ?? (ctx as any).channelId ?? '',
|
|
411
446
|
sender: event.sender ?? '',
|
|
412
447
|
content: event.content ?? '',
|
|
413
448
|
metadata: event.metadata ?? {},
|
|
414
|
-
}).catch(() =>
|
|
449
|
+
}).catch((e) => log.warn('[SafeClaw] Failed to evaluate inbound message:', e));
|
|
415
450
|
});
|
|
416
451
|
|
|
417
452
|
// Agent tools — let agents introspect governance state (#197)
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "safeclaw",
|
|
3
3
|
"name": "SafeClaw Neurosymbolic Governance",
|
|
4
4
|
"description": "Validates AI agent actions against OWL ontologies and SHACL constraints before execution",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.5.0",
|
|
6
6
|
"author": "Tendly EU",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"homepage": "https://safeclaw.eu",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-safeclaw-plugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
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
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* and exposes helpers for saving and hashing config state.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
9
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync, chmodSync } from 'fs';
|
|
10
10
|
import { join, dirname } from 'path';
|
|
11
11
|
import { homedir } from 'os';
|
|
12
12
|
import crypto from 'crypto';
|
|
@@ -181,6 +181,7 @@ export function saveConfig(config: SafeClawConfig): void {
|
|
|
181
181
|
|
|
182
182
|
try {
|
|
183
183
|
writeFileSync(CONFIG_PATH, JSON.stringify(existing, null, 2) + '\n', { encoding: 'utf-8', mode: 0o600 });
|
|
184
|
+
try { chmodSync(CONFIG_PATH, 0o600); } catch { /* best-effort */ }
|
|
184
185
|
} catch (e) {
|
|
185
186
|
const code = (e as NodeJS.ErrnoException).code;
|
|
186
187
|
if (code === 'EROFS') {
|
|
@@ -203,6 +204,7 @@ export function configHash(config: SafeClawConfig): string {
|
|
|
203
204
|
enforcement: config.enforcement,
|
|
204
205
|
failMode: config.failMode,
|
|
205
206
|
serviceUrl: config.serviceUrl,
|
|
207
|
+
apiKeyFingerprint: config.apiKey ? config.apiKey.slice(0, 4) + config.apiKey.slice(-4) : '',
|
|
206
208
|
});
|
|
207
209
|
return crypto.createHash('sha256').update(payload).digest('hex');
|
|
208
210
|
}
|
package/types/openclaw-sdk.d.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Ambient type declarations for OpenClaw Plugin SDK.
|
|
3
|
-
* These match the types exported by openclaw/plugin-sdk as of v2026.
|
|
3
|
+
* These match the types exported by openclaw/plugin-sdk as of v2026.4.
|
|
4
4
|
* When installed inside OpenClaw, the real SDK types take precedence.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
declare module 'openclaw/plugin-sdk/core' {
|
|
8
8
|
export interface OpenClawPluginApi {
|
|
9
|
+
/**
|
|
10
|
+
* Register a hook handler for OpenClaw lifecycle events.
|
|
11
|
+
*
|
|
12
|
+
* @deprecated The `before_agent_start` hook is deprecated in v2026.4.
|
|
13
|
+
* Use `before_model_resolve` + `before_prompt_build` instead.
|
|
14
|
+
*/
|
|
9
15
|
on(
|
|
10
16
|
hookName: string,
|
|
11
17
|
handler: (event: OpenClawPluginEvent, ctx: OpenClawPluginContext) => Promise<Record<string, unknown> | void> | void,
|
|
@@ -33,6 +39,9 @@ declare module 'openclaw/plugin-sdk/core' {
|
|
|
33
39
|
model?: string;
|
|
34
40
|
lastAssistant?: string;
|
|
35
41
|
usage?: Record<string, unknown>;
|
|
42
|
+
runId?: string;
|
|
43
|
+
toolCallId?: string;
|
|
44
|
+
childAgentId?: string;
|
|
36
45
|
parentAgentId?: string;
|
|
37
46
|
childConfig?: Record<string, unknown>;
|
|
38
47
|
reason?: string;
|
|
@@ -66,6 +75,32 @@ declare module 'openclaw/plugin-sdk/core' {
|
|
|
66
75
|
execute: (params: Record<string, unknown>, ctx: Record<string, unknown>) => Promise<unknown>;
|
|
67
76
|
}
|
|
68
77
|
|
|
78
|
+
export interface PluginApprovalRequest {
|
|
79
|
+
title: string;
|
|
80
|
+
description: string;
|
|
81
|
+
severity?: 'info' | 'warning' | 'critical';
|
|
82
|
+
timeoutMs?: number;
|
|
83
|
+
timeoutBehavior?: 'allow' | 'deny';
|
|
84
|
+
pluginId?: string;
|
|
85
|
+
onResolution?: (decision: PluginApprovalResolution) => Promise<void> | void;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface PluginApprovalResolution {
|
|
89
|
+
approved: boolean;
|
|
90
|
+
resolvedBy?: string;
|
|
91
|
+
resolvedAt?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Hook result types for before_tool_call.
|
|
96
|
+
* Return one of: nothing (allow), { block, blockReason }, or { requireApproval }.
|
|
97
|
+
*/
|
|
98
|
+
export interface BeforeToolCallResult {
|
|
99
|
+
block?: boolean;
|
|
100
|
+
blockReason?: string;
|
|
101
|
+
requireApproval?: PluginApprovalRequest;
|
|
102
|
+
}
|
|
103
|
+
|
|
69
104
|
export interface OpenClawPluginLogger {
|
|
70
105
|
info: (...args: unknown[]) => void;
|
|
71
106
|
warn: (...args: unknown[]) => void;
|