openclaw-safeclaw-plugin 1.4.0 → 1.4.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/dist/index.js +39 -18
- package/dist/tui/config.js +6 -1
- package/index.ts +42 -19
- package/package.json +1 -1
- package/tui/config.ts +3 -1
- package/types/openclaw-sdk.d.ts +36 -1
package/dist/index.js
CHANGED
|
@@ -99,6 +99,7 @@ async function get(path) {
|
|
|
99
99
|
}
|
|
100
100
|
// --- Plugin Definition ---
|
|
101
101
|
let handshakeCompleted = false;
|
|
102
|
+
let lastHandshakeConfigHash = '';
|
|
102
103
|
async function performHandshake() {
|
|
103
104
|
const cfg = getConfig();
|
|
104
105
|
if (!cfg.apiKey) {
|
|
@@ -115,6 +116,7 @@ async function performHandshake() {
|
|
|
115
116
|
}
|
|
116
117
|
log.info(`[SafeClaw] Handshake OK — org=${r.orgId}, scope=${r.scope}, engine=${r.engineReady ? 'ready' : 'not ready'}`);
|
|
117
118
|
handshakeCompleted = true;
|
|
119
|
+
lastHandshakeConfigHash = configHash(getConfig());
|
|
118
120
|
return true;
|
|
119
121
|
}
|
|
120
122
|
async function checkConnection() {
|
|
@@ -160,9 +162,15 @@ export default {
|
|
|
160
162
|
// Heartbeat watchdog — send config hash to service every 30s
|
|
161
163
|
const sendHeartbeat = async () => {
|
|
162
164
|
try {
|
|
165
|
+
const currentHash = configHash(getConfig());
|
|
166
|
+
if (handshakeCompleted && currentHash !== lastHandshakeConfigHash) {
|
|
167
|
+
log.info('[SafeClaw] Config changed — re-authenticating');
|
|
168
|
+
handshakeCompleted = false;
|
|
169
|
+
performHandshake().catch(() => { });
|
|
170
|
+
}
|
|
163
171
|
await post('/heartbeat', {
|
|
164
172
|
agentId: instanceId,
|
|
165
|
-
configHash:
|
|
173
|
+
configHash: currentHash,
|
|
166
174
|
status: 'alive',
|
|
167
175
|
});
|
|
168
176
|
}
|
|
@@ -237,6 +245,19 @@ export default {
|
|
|
237
245
|
else if (r === null && cfg.failMode === 'closed' && cfg.enforcement === 'audit-only') {
|
|
238
246
|
log.warn(`[SafeClaw] Service unavailable at ${cfg.serviceUrl} (fail-closed mode, audit-only)`);
|
|
239
247
|
}
|
|
248
|
+
// If service says confirmation required, use OpenClaw's native approval flow
|
|
249
|
+
if (r?.confirmationRequired) {
|
|
250
|
+
const riskLevel = r.riskLevel || '';
|
|
251
|
+
return {
|
|
252
|
+
requireApproval: {
|
|
253
|
+
title: 'SafeClaw Governance Check',
|
|
254
|
+
description: r.reason || 'This action requires confirmation',
|
|
255
|
+
severity: riskLevel === 'HighRisk' ? 'critical' : riskLevel === 'MediumRisk' ? 'warning' : 'info',
|
|
256
|
+
timeoutMs: 30_000,
|
|
257
|
+
timeoutBehavior: riskLevel === 'HighRisk' ? 'deny' : 'allow',
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
}
|
|
240
261
|
if (r?.block) {
|
|
241
262
|
const blockReason = r.reason || 'Blocked by SafeClaw (no reason provided)';
|
|
242
263
|
if (cfg.enforcement === 'enforce') {
|
|
@@ -300,7 +321,7 @@ export default {
|
|
|
300
321
|
content: event.prompt ?? '',
|
|
301
322
|
provider: event.provider ?? '',
|
|
302
323
|
model: event.model ?? '',
|
|
303
|
-
}).catch(() =>
|
|
324
|
+
}).catch((e) => log.warn('[SafeClaw] Failed to log LLM input:', e));
|
|
304
325
|
});
|
|
305
326
|
api.on('llm_output', (event, ctx) => {
|
|
306
327
|
post('/log/llm-output', {
|
|
@@ -309,7 +330,7 @@ export default {
|
|
|
309
330
|
provider: event.provider ?? '',
|
|
310
331
|
model: event.model ?? '',
|
|
311
332
|
usage: event.usage ?? {},
|
|
312
|
-
}).catch(() =>
|
|
333
|
+
}).catch((e) => log.warn('[SafeClaw] Failed to log LLM output:', e));
|
|
313
334
|
});
|
|
314
335
|
// (#195: use event.toolName, !event.error for success, add durationMs and error)
|
|
315
336
|
api.on('after_tool_call', (event, ctx) => {
|
|
@@ -327,15 +348,15 @@ export default {
|
|
|
327
348
|
api.on('subagent_spawning', async (event, ctx) => {
|
|
328
349
|
const cfg = getConfig();
|
|
329
350
|
const r = await post('/evaluate/subagent-spawn', {
|
|
330
|
-
sessionId: ctx.sessionId ?? event.sessionId,
|
|
331
|
-
userId: ctx.agentId,
|
|
351
|
+
sessionId: ctx.sessionId ?? event.sessionId ?? '',
|
|
352
|
+
userId: ctx.agentId ?? '',
|
|
332
353
|
parentAgentId: event.parentAgentId,
|
|
333
354
|
childConfig: event.childConfig ?? {},
|
|
334
355
|
reason: event.reason ?? '',
|
|
335
356
|
});
|
|
336
357
|
// Fail-closed handling (matches before_tool_call / message_sending pattern)
|
|
337
358
|
if (r === null && cfg.failMode === 'closed' && cfg.enforcement === 'enforce') {
|
|
338
|
-
|
|
359
|
+
return { status: 'error', error: 'SafeClaw service unavailable (fail-closed mode)' };
|
|
339
360
|
}
|
|
340
361
|
else if (r === null && cfg.failMode === 'closed' && cfg.enforcement === 'warn-only') {
|
|
341
362
|
log.warn('[SafeClaw] Service unavailable (fail-closed mode, warn-only)');
|
|
@@ -344,7 +365,7 @@ export default {
|
|
|
344
365
|
log.warn('[SafeClaw] Service unavailable (fail-closed mode, audit-only)');
|
|
345
366
|
}
|
|
346
367
|
if (r?.block && cfg.enforcement === 'enforce') {
|
|
347
|
-
|
|
368
|
+
return { status: 'error', error: r.reason || 'Blocked by SafeClaw: delegation bypass detected' };
|
|
348
369
|
}
|
|
349
370
|
if (r?.block && cfg.enforcement === 'warn-only') {
|
|
350
371
|
log.warn(`[SafeClaw] Subagent spawn warning: ${r.reason}`);
|
|
@@ -353,38 +374,38 @@ export default {
|
|
|
353
374
|
// Subagent ended — record child agent lifecycle (#188)
|
|
354
375
|
api.on('subagent_ended', (event, ctx) => {
|
|
355
376
|
post('/record/subagent-ended', {
|
|
356
|
-
sessionId: ctx.sessionId ?? event.sessionId,
|
|
377
|
+
sessionId: ctx.sessionId ?? event.sessionId ?? '',
|
|
357
378
|
parentAgentId: event.parentAgentId,
|
|
358
379
|
childAgentId: event.childAgentId,
|
|
359
|
-
}).catch(() =>
|
|
380
|
+
}).catch((e) => log.warn('[SafeClaw] Failed to record subagent ended:', e));
|
|
360
381
|
});
|
|
361
382
|
// Session lifecycle — notify service of session start (#189)
|
|
362
383
|
api.on('session_start', (event, ctx) => {
|
|
363
384
|
post('/session/start', {
|
|
364
|
-
sessionId: ctx.sessionId ?? event.sessionId,
|
|
365
|
-
userId: ctx.agentId,
|
|
385
|
+
sessionId: ctx.sessionId ?? event.sessionId ?? '',
|
|
386
|
+
userId: ctx.agentId ?? '',
|
|
366
387
|
agentId: instanceId,
|
|
367
388
|
metadata: event.metadata ?? {},
|
|
368
|
-
}).catch(() =>
|
|
389
|
+
}).catch((e) => log.warn('[SafeClaw] Failed to record session start:', e));
|
|
369
390
|
});
|
|
370
391
|
// Session lifecycle — notify service of session end (#189)
|
|
371
392
|
api.on('session_end', (event, ctx) => {
|
|
372
393
|
post('/session/end', {
|
|
373
|
-
sessionId: ctx.sessionId ?? event.sessionId,
|
|
374
|
-
userId: ctx.agentId,
|
|
394
|
+
sessionId: ctx.sessionId ?? event.sessionId ?? '',
|
|
395
|
+
userId: ctx.agentId ?? '',
|
|
375
396
|
agentId: instanceId,
|
|
376
|
-
}).catch(() =>
|
|
397
|
+
}).catch((e) => log.warn('[SafeClaw] Failed to record session end:', e));
|
|
377
398
|
});
|
|
378
399
|
// Inbound message governance — evaluate received messages (#190)
|
|
379
400
|
api.on('message_received', (event, ctx) => {
|
|
380
401
|
post('/evaluate/inbound-message', {
|
|
381
|
-
sessionId: ctx.sessionId ?? event.sessionId,
|
|
382
|
-
userId: ctx.agentId,
|
|
402
|
+
sessionId: ctx.sessionId ?? event.sessionId ?? '',
|
|
403
|
+
userId: ctx.agentId ?? '',
|
|
383
404
|
channel: event.channel ?? ctx.channelId ?? '',
|
|
384
405
|
sender: event.sender ?? '',
|
|
385
406
|
content: event.content ?? '',
|
|
386
407
|
metadata: event.metadata ?? {},
|
|
387
|
-
}).catch(() =>
|
|
408
|
+
}).catch((e) => log.warn('[SafeClaw] Failed to evaluate inbound message:', e));
|
|
388
409
|
});
|
|
389
410
|
// Agent tools — let agents introspect governance state (#197)
|
|
390
411
|
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';
|
|
@@ -107,6 +107,7 @@ async function get(path: string): Promise<Record<string, unknown> | null> {
|
|
|
107
107
|
// --- Plugin Definition ---
|
|
108
108
|
|
|
109
109
|
let handshakeCompleted = false;
|
|
110
|
+
let lastHandshakeConfigHash = '';
|
|
110
111
|
|
|
111
112
|
async function performHandshake(): Promise<boolean> {
|
|
112
113
|
const cfg = getConfig();
|
|
@@ -127,6 +128,7 @@ async function performHandshake(): Promise<boolean> {
|
|
|
127
128
|
|
|
128
129
|
log.info(`[SafeClaw] Handshake OK — org=${r.orgId}, scope=${r.scope}, engine=${r.engineReady ? 'ready' : 'not ready'}`);
|
|
129
130
|
handshakeCompleted = true;
|
|
131
|
+
lastHandshakeConfigHash = configHash(getConfig());
|
|
130
132
|
return true;
|
|
131
133
|
}
|
|
132
134
|
|
|
@@ -176,9 +178,15 @@ export default {
|
|
|
176
178
|
// Heartbeat watchdog — send config hash to service every 30s
|
|
177
179
|
const sendHeartbeat = async () => {
|
|
178
180
|
try {
|
|
181
|
+
const currentHash = configHash(getConfig());
|
|
182
|
+
if (handshakeCompleted && currentHash !== lastHandshakeConfigHash) {
|
|
183
|
+
log.info('[SafeClaw] Config changed — re-authenticating');
|
|
184
|
+
handshakeCompleted = false;
|
|
185
|
+
performHandshake().catch(() => {});
|
|
186
|
+
}
|
|
179
187
|
await post('/heartbeat', {
|
|
180
188
|
agentId: instanceId,
|
|
181
|
-
configHash:
|
|
189
|
+
configHash: currentHash,
|
|
182
190
|
status: 'alive',
|
|
183
191
|
});
|
|
184
192
|
} catch {
|
|
@@ -254,6 +262,21 @@ export default {
|
|
|
254
262
|
} else if (r === null && cfg.failMode === 'closed' && cfg.enforcement === 'audit-only') {
|
|
255
263
|
log.warn(`[SafeClaw] Service unavailable at ${cfg.serviceUrl} (fail-closed mode, audit-only)`);
|
|
256
264
|
}
|
|
265
|
+
|
|
266
|
+
// If service says confirmation required, use OpenClaw's native approval flow
|
|
267
|
+
if (r?.confirmationRequired) {
|
|
268
|
+
const riskLevel = (r.riskLevel as string) || '';
|
|
269
|
+
return {
|
|
270
|
+
requireApproval: {
|
|
271
|
+
title: 'SafeClaw Governance Check',
|
|
272
|
+
description: (r.reason as string) || 'This action requires confirmation',
|
|
273
|
+
severity: riskLevel === 'HighRisk' ? 'critical' : riskLevel === 'MediumRisk' ? 'warning' : 'info',
|
|
274
|
+
timeoutMs: 30_000,
|
|
275
|
+
timeoutBehavior: riskLevel === 'HighRisk' ? 'deny' : 'allow',
|
|
276
|
+
},
|
|
277
|
+
} satisfies BeforeToolCallResult;
|
|
278
|
+
}
|
|
279
|
+
|
|
257
280
|
if (r?.block) {
|
|
258
281
|
const blockReason = (r.reason as string) || 'Blocked by SafeClaw (no reason provided)';
|
|
259
282
|
if (cfg.enforcement === 'enforce') {
|
|
@@ -320,7 +343,7 @@ export default {
|
|
|
320
343
|
content: event.prompt ?? '',
|
|
321
344
|
provider: event.provider ?? '',
|
|
322
345
|
model: event.model ?? '',
|
|
323
|
-
}).catch(() =>
|
|
346
|
+
}).catch((e) => log.warn('[SafeClaw] Failed to log LLM input:', e));
|
|
324
347
|
});
|
|
325
348
|
|
|
326
349
|
api.on('llm_output', (event: OpenClawPluginEvent, ctx: OpenClawPluginContext) => {
|
|
@@ -330,7 +353,7 @@ export default {
|
|
|
330
353
|
provider: event.provider ?? '',
|
|
331
354
|
model: event.model ?? '',
|
|
332
355
|
usage: event.usage ?? {},
|
|
333
|
-
}).catch(() =>
|
|
356
|
+
}).catch((e) => log.warn('[SafeClaw] Failed to log LLM output:', e));
|
|
334
357
|
});
|
|
335
358
|
|
|
336
359
|
// (#195: use event.toolName, !event.error for success, add durationMs and error)
|
|
@@ -350,8 +373,8 @@ export default {
|
|
|
350
373
|
api.on('subagent_spawning', async (event: OpenClawPluginEvent, ctx: OpenClawPluginContext) => {
|
|
351
374
|
const cfg = getConfig();
|
|
352
375
|
const r = await post('/evaluate/subagent-spawn', {
|
|
353
|
-
sessionId: ctx.sessionId ?? event.sessionId,
|
|
354
|
-
userId: ctx.agentId,
|
|
376
|
+
sessionId: ctx.sessionId ?? event.sessionId ?? '',
|
|
377
|
+
userId: ctx.agentId ?? '',
|
|
355
378
|
parentAgentId: event.parentAgentId,
|
|
356
379
|
childConfig: event.childConfig ?? {},
|
|
357
380
|
reason: event.reason ?? '',
|
|
@@ -359,7 +382,7 @@ export default {
|
|
|
359
382
|
|
|
360
383
|
// Fail-closed handling (matches before_tool_call / message_sending pattern)
|
|
361
384
|
if (r === null && cfg.failMode === 'closed' && cfg.enforcement === 'enforce') {
|
|
362
|
-
|
|
385
|
+
return { status: 'error', error: 'SafeClaw service unavailable (fail-closed mode)' };
|
|
363
386
|
} else if (r === null && cfg.failMode === 'closed' && cfg.enforcement === 'warn-only') {
|
|
364
387
|
log.warn('[SafeClaw] Service unavailable (fail-closed mode, warn-only)');
|
|
365
388
|
} else if (r === null && cfg.failMode === 'closed' && cfg.enforcement === 'audit-only') {
|
|
@@ -367,7 +390,7 @@ export default {
|
|
|
367
390
|
}
|
|
368
391
|
|
|
369
392
|
if (r?.block && cfg.enforcement === 'enforce') {
|
|
370
|
-
|
|
393
|
+
return { status: 'error', error: (r.reason as string) || 'Blocked by SafeClaw: delegation bypass detected' };
|
|
371
394
|
}
|
|
372
395
|
if (r?.block && cfg.enforcement === 'warn-only') {
|
|
373
396
|
log.warn(`[SafeClaw] Subagent spawn warning: ${r.reason}`);
|
|
@@ -377,41 +400,41 @@ export default {
|
|
|
377
400
|
// Subagent ended — record child agent lifecycle (#188)
|
|
378
401
|
api.on('subagent_ended', (event: OpenClawPluginEvent, ctx: OpenClawPluginContext) => {
|
|
379
402
|
post('/record/subagent-ended', {
|
|
380
|
-
sessionId: ctx.sessionId ?? event.sessionId,
|
|
403
|
+
sessionId: ctx.sessionId ?? event.sessionId ?? '',
|
|
381
404
|
parentAgentId: event.parentAgentId,
|
|
382
405
|
childAgentId: event.childAgentId,
|
|
383
|
-
}).catch(() =>
|
|
406
|
+
}).catch((e) => log.warn('[SafeClaw] Failed to record subagent ended:', e));
|
|
384
407
|
});
|
|
385
408
|
|
|
386
409
|
// Session lifecycle — notify service of session start (#189)
|
|
387
410
|
api.on('session_start', (event: OpenClawPluginEvent, ctx: OpenClawPluginContext) => {
|
|
388
411
|
post('/session/start', {
|
|
389
|
-
sessionId: ctx.sessionId ?? event.sessionId,
|
|
390
|
-
userId: ctx.agentId,
|
|
412
|
+
sessionId: ctx.sessionId ?? event.sessionId ?? '',
|
|
413
|
+
userId: ctx.agentId ?? '',
|
|
391
414
|
agentId: instanceId,
|
|
392
415
|
metadata: event.metadata ?? {},
|
|
393
|
-
}).catch(() =>
|
|
416
|
+
}).catch((e) => log.warn('[SafeClaw] Failed to record session start:', e));
|
|
394
417
|
});
|
|
395
418
|
|
|
396
419
|
// Session lifecycle — notify service of session end (#189)
|
|
397
420
|
api.on('session_end', (event: OpenClawPluginEvent, ctx: OpenClawPluginContext) => {
|
|
398
421
|
post('/session/end', {
|
|
399
|
-
sessionId: ctx.sessionId ?? event.sessionId,
|
|
400
|
-
userId: ctx.agentId,
|
|
422
|
+
sessionId: ctx.sessionId ?? event.sessionId ?? '',
|
|
423
|
+
userId: ctx.agentId ?? '',
|
|
401
424
|
agentId: instanceId,
|
|
402
|
-
}).catch(() =>
|
|
425
|
+
}).catch((e) => log.warn('[SafeClaw] Failed to record session end:', e));
|
|
403
426
|
});
|
|
404
427
|
|
|
405
428
|
// Inbound message governance — evaluate received messages (#190)
|
|
406
429
|
api.on('message_received', (event: OpenClawPluginEvent, ctx: OpenClawPluginContext) => {
|
|
407
430
|
post('/evaluate/inbound-message', {
|
|
408
|
-
sessionId: ctx.sessionId ?? event.sessionId,
|
|
409
|
-
userId: ctx.agentId,
|
|
431
|
+
sessionId: ctx.sessionId ?? event.sessionId ?? '',
|
|
432
|
+
userId: ctx.agentId ?? '',
|
|
410
433
|
channel: event.channel ?? (ctx as any).channelId ?? '',
|
|
411
434
|
sender: event.sender ?? '',
|
|
412
435
|
content: event.content ?? '',
|
|
413
436
|
metadata: event.metadata ?? {},
|
|
414
|
-
}).catch(() =>
|
|
437
|
+
}).catch((e) => log.warn('[SafeClaw] Failed to evaluate inbound message:', e));
|
|
415
438
|
});
|
|
416
439
|
|
|
417
440
|
// Agent tools — let agents introspect governance state (#197)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-safeclaw-plugin",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.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
|
@@ -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;
|