openclaw-safeclaw-plugin 1.5.0 → 2.0.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/README.md CHANGED
@@ -130,7 +130,7 @@ The plugin registers 11 hooks on OpenClaw events. Each hook communicates with th
130
130
  |------|----------|-------------|
131
131
  | `before_tool_call` | 100 | The main gate. Evaluates every tool call against SHACL shapes, policies, preferences, and dependencies. Returns `{ block: true }` if the action violates constraints. |
132
132
  | `message_sending` | 100 | Checks outbound messages for sensitive data leaks, contact rule violations, and content policy. Returns `{ cancel: true }` to block. |
133
- | `subagent_spawning` | 100 | Evaluates child agent spawn requests. Detects delegation bypass attempts where a blocked parent tries to spawn an unrestricted child. |
133
+ | `subagent_spawning` | 100 | Forwards child-spawn requests (parent/child session keys, child agent id) to the service. **Note:** under OpenClaw v2026.6.8 this hook exposes only session keys — no parent agent id — so spawn-time delegation-bypass *enforcement* is deferred to the session-keyed hierarchy work (#321); today this hook is observational. Child tool calls are still gated individually by `before_tool_call`. |
134
134
 
135
135
  ### Context hooks (modify agent behavior)
136
136
 
@@ -150,6 +150,16 @@ The plugin registers 11 hooks on OpenClaw events. Each hook communicates with th
150
150
  | `session_end` | Notifies the service when a session ends. |
151
151
  | `message_received` | Evaluates inbound messages for governance (sender, channel, content). |
152
152
 
153
+ > **Required for LLM audit logging.** As of OpenClaw v2026.6.8, raw-conversation
154
+ > hooks (`llm_input`, `llm_output`, and the `before_agent_*` family) only fire for
155
+ > non-bundled plugins when conversation access is explicitly granted. Without it,
156
+ > SafeClaw's LLM I/O audit trail is silently empty. Enable it in your OpenClaw
157
+ > config:
158
+ >
159
+ > ```json
160
+ > { "plugins": { "entries": { "safeclaw": { "hooks": { "allowConversationAccess": true } } } } }
161
+ > ```
162
+
153
163
  ## Agent Tools
154
164
 
155
165
  The plugin registers two tools that agents can call to introspect governance state.
package/dist/index.js CHANGED
@@ -107,6 +107,13 @@ function approvalSeverityForRisk(riskLevel) {
107
107
  function approvalTimeoutBehaviorForRisk(riskLevel) {
108
108
  return riskLevel === 'CriticalRisk' || riskLevel === 'HighRisk' ? 'deny' : 'allow';
109
109
  }
110
+ // For high-risk actions, forbid durable "allow-always" trust — the user may only
111
+ // approve this one occurrence or deny (#314). Lower-risk actions keep all options.
112
+ function approvalAllowedDecisionsForRisk(riskLevel) {
113
+ return riskLevel === 'CriticalRisk' || riskLevel === 'HighRisk'
114
+ ? ['allow-once', 'deny']
115
+ : ['allow-once', 'allow-always', 'deny'];
116
+ }
110
117
  // --- Plugin Definition ---
111
118
  let handshakeCompleted = false;
112
119
  let lastHandshakeConfigHash = '';
@@ -240,11 +247,19 @@ export default {
240
247
  return { block: true, blockReason: 'SafeClaw handshake not completed (fail-closed)' };
241
248
  }
242
249
  const r = await post('/evaluate/tool-call', {
243
- sessionId: ctx.sessionId ?? event.sessionId ?? '',
250
+ sessionId: ctx.sessionKey ?? ctx.sessionId ?? event.sessionId ?? '',
244
251
  userId: ctx.agentId ?? '',
245
252
  toolName: event.toolName ?? '',
246
253
  params: event.params ?? {},
247
254
  runId: ctx.runId ?? '',
255
+ // v2026.6.8 discriminators so the service can classify code-mode exec,
256
+ // file-touching envelopes, etc. (#316)
257
+ toolKind: event.toolKind ?? '',
258
+ toolInputKind: event.toolInputKind ?? '',
259
+ derivedPaths: event.derivedPaths ?? [],
260
+ // Trigger origin (#324): cron-driven runs carry ctx.jobId.
261
+ triggeredBy: ctx.jobId ? 'cron' : 'interactive',
262
+ jobId: ctx.jobId ?? '',
248
263
  });
249
264
  if (r === null && cfg.failMode === 'closed' && cfg.enforcement === 'enforce') {
250
265
  return { block: true, blockReason: `SafeClaw service unavailable at ${cfg.serviceUrl} (fail-closed)` };
@@ -255,18 +270,26 @@ export default {
255
270
  else if (r === null && cfg.failMode === 'closed' && cfg.enforcement === 'audit-only') {
256
271
  log.warn(`[SafeClaw] Service unavailable at ${cfg.serviceUrl} (fail-closed mode, audit-only)`);
257
272
  }
258
- // If service says confirmation required, use OpenClaw's native approval flow
273
+ // If service says confirmation required, use OpenClaw's native approval flow.
274
+ // Carry any param rewrite (#316) on the same result — OpenClaw applies it as
275
+ // the approval's overrideParams once the user approves, so the post-approval
276
+ // execution still runs the governed (sanitized) params.
259
277
  if (r?.confirmationRequired) {
260
278
  const riskLevel = r.riskLevel || '';
261
- return {
279
+ const result = {
262
280
  requireApproval: {
263
281
  title: 'SafeClaw Governance Check',
264
282
  description: r.reason || 'This action requires confirmation',
265
283
  severity: approvalSeverityForRisk(riskLevel),
266
284
  timeoutMs: 30_000,
267
285
  timeoutBehavior: approvalTimeoutBehaviorForRisk(riskLevel),
286
+ allowedDecisions: approvalAllowedDecisionsForRisk(riskLevel),
268
287
  },
269
288
  };
289
+ if (r.params && typeof r.params === 'object') {
290
+ result.params = r.params;
291
+ }
292
+ return result;
270
293
  }
271
294
  if (r?.block) {
272
295
  const blockReason = r.reason || 'Blocked by SafeClaw (no reason provided)';
@@ -278,24 +301,31 @@ export default {
278
301
  }
279
302
  // audit-only: logged server-side, no action here
280
303
  }
304
+ // Param rewrite (#316): the service only returns `params` for an allowed
305
+ // call when it sanitized the input (e.g. stripped control characters), so
306
+ // the tool executes the governed params rather than the raw ones.
307
+ if (r?.params && typeof r.params === 'object') {
308
+ return { params: r.params };
309
+ }
281
310
  }, { priority: 100 });
282
311
  // Context injection — prepend governance context to agent system prompt
283
312
  // (#195: before_agent_start is deprecated; use before_prompt_build + prependSystemContext)
284
313
  api.on('before_prompt_build', async (event, ctx) => {
285
314
  const r = await post('/context/build', {
286
- sessionId: ctx.sessionId ?? event.sessionId ?? '',
315
+ sessionId: ctx.sessionKey ?? ctx.sessionId ?? event.sessionId ?? '',
287
316
  userId: ctx.agentId ?? '',
288
317
  });
289
318
  if (r?.prependContext) {
290
319
  return { prependSystemContext: r.prependContext };
291
320
  }
292
321
  }, { priority: 100 });
293
- // Message governance — check outbound messages
294
- // (#195: use ctx.conversationId/sessionId, ctx.accountId; return only { cancel: true })
322
+ // Message governance — check outbound messages.
323
+ // (v2026.6.8 message context exposes sessionKey/accountId/channelId there is
324
+ // no sessionId; the event has no session field. Return only { cancel: true }.)
295
325
  api.on('message_sending', async (event, ctx) => {
296
326
  const cfg = getConfig();
297
327
  const r = await post('/evaluate/message', {
298
- sessionId: ctx.conversationId ?? ctx.sessionId ?? event.sessionId ?? '',
328
+ sessionId: ctx.sessionKey ?? ctx.conversationId ?? '',
299
329
  userId: ctx.accountId ?? '',
300
330
  to: event.to,
301
331
  content: event.content,
@@ -323,29 +353,42 @@ export default {
323
353
  // audit-only: logged server-side, no action here
324
354
  }
325
355
  }, { priority: 100 });
326
- // Async logging — fire-and-forget, no return value needed
327
- // (#195: use event.prompt for input, event.lastAssistant for output; add provider/model)
356
+ // Async logging — fire-and-forget, no return value needed.
357
+ // (#315: v2026.6.8 raw-conversation hooks require
358
+ // `plugins.entries.safeclaw.hooks.allowConversationAccess: true` to fire at
359
+ // all — see README. Output text is `assistantTexts: string[]`, not the
360
+ // untyped `lastAssistant`. `runId` is forwarded for audit correlation.)
328
361
  api.on('llm_input', (event, ctx) => {
329
362
  post('/log/llm-input', {
330
- sessionId: event.sessionId ?? ctx.sessionId ?? '',
363
+ sessionId: ctx.sessionKey ?? event.sessionId ?? ctx.sessionId ?? '',
331
364
  content: event.prompt ?? '',
332
365
  provider: event.provider ?? '',
333
366
  model: event.model ?? '',
367
+ runId: event.runId ?? ctx.runId ?? '',
334
368
  }).catch((e) => log.warn('[SafeClaw] Failed to log LLM input:', e));
335
369
  });
336
370
  api.on('llm_output', (event, ctx) => {
371
+ // Join assistant segments with a blank line, matching how OpenClaw v2026.6.8
372
+ // itself joins `assistantTexts`, so message boundaries are preserved in the
373
+ // audit content (joining with '' would fuse "first"+"second" into one blob).
374
+ const assistantText = Array.isArray(event.assistantTexts)
375
+ ? event.assistantTexts.join('\n\n')
376
+ : typeof event.lastAssistant === 'string'
377
+ ? event.lastAssistant
378
+ : '';
337
379
  post('/log/llm-output', {
338
- sessionId: event.sessionId ?? ctx.sessionId ?? '',
339
- content: event.lastAssistant ?? '',
380
+ sessionId: ctx.sessionKey ?? event.sessionId ?? ctx.sessionId ?? '',
381
+ content: assistantText,
340
382
  provider: event.provider ?? '',
341
383
  model: event.model ?? '',
342
384
  usage: event.usage ?? {},
385
+ runId: event.runId ?? ctx.runId ?? '',
343
386
  }).catch((e) => log.warn('[SafeClaw] Failed to log LLM output:', e));
344
387
  });
345
388
  // (#195: use event.toolName, !event.error for success, add durationMs and error)
346
389
  api.on('after_tool_call', (event, ctx) => {
347
390
  post('/record/tool-result', {
348
- sessionId: ctx.sessionId ?? event.sessionId ?? '',
391
+ sessionId: ctx.sessionKey ?? ctx.sessionId ?? event.sessionId ?? '',
349
392
  userId: ctx.agentId ?? '',
350
393
  toolName: event.toolName ?? '',
351
394
  params: event.params ?? {},
@@ -356,15 +399,27 @@ export default {
356
399
  runId: ctx.runId ?? event.runId ?? '',
357
400
  }).catch((e) => log.warn('[SafeClaw] Failed to record tool result:', e));
358
401
  });
359
- // Subagent governance — block delegation bypass attempts (#188)
402
+ // Subagent governance — block delegation bypass attempts (#188).
403
+ // NOTE: `subagent_spawning` is a deprecated OpenClaw compatibility hook
404
+ // (removal scheduled after 2026-08-16). In v2026.6.8 the context exposes only
405
+ // session keys (`requesterSessionKey`/`childSessionKey`/`runId`) — there is NO
406
+ // parent agentId at this hook. The event carries the *child's* `agentId`. So
407
+ // we forward the parent/child session keys and the child agent id; the service
408
+ // cannot do the killed-PARENT lookup from a session key alone, so spawn-time
409
+ // delegation enforcement is reworked onto a session-keyed hierarchy in #321.
410
+ // `requester` is an object (channel/account/to/threadId), not a string. (#312)
360
411
  api.on('subagent_spawning', async (event, ctx) => {
361
412
  const cfg = getConfig();
362
413
  const r = await post('/evaluate/subagent-spawn', {
363
- sessionId: ctx.sessionId ?? event.sessionId ?? '',
364
- userId: ctx.agentId ?? '',
365
- parentAgentId: event.parentAgentId,
366
- childConfig: event.childConfig ?? {},
367
- reason: event.reason ?? '',
414
+ sessionId: ctx.requesterSessionKey ?? ctx.childSessionKey ?? '',
415
+ parentSessionKey: ctx.requesterSessionKey ?? '',
416
+ childSessionKey: ctx.childSessionKey ?? event.childSessionKey ?? '',
417
+ childAgentId: event.agentId ?? '',
418
+ mode: event.mode ?? '',
419
+ threadRequested: event.threadRequested ?? false,
420
+ requester: event.requester ?? {},
421
+ runId: ctx.runId ?? '',
422
+ reason: event.label ?? '',
368
423
  });
369
424
  // Fail-closed handling (matches before_tool_call / message_sending pattern)
370
425
  if (r === null && cfg.failMode === 'closed' && cfg.enforcement === 'enforce') {
@@ -383,18 +438,26 @@ export default {
383
438
  log.warn(`[SafeClaw] Subagent spawn warning: ${r.reason}`);
384
439
  }
385
440
  }, { priority: 100 });
386
- // Subagent ended — record child agent lifecycle (#188)
441
+ // Subagent ended — record child agent lifecycle (#188).
442
+ // v2026.6.8 reports `targetSessionKey`/`targetKind`/`outcome`/`error`/`runId`,
443
+ // not parent/child agent IDs. The ended child is `targetSessionKey`; the
444
+ // parent is the requester session key from the subagent context. (#312)
387
445
  api.on('subagent_ended', (event, ctx) => {
388
446
  post('/record/subagent-ended', {
389
- sessionId: ctx.sessionId ?? event.sessionId ?? '',
390
- parentAgentId: event.parentAgentId,
391
- childAgentId: event.childAgentId,
447
+ sessionId: ctx.requesterSessionKey ?? event.targetSessionKey ?? '',
448
+ parentSessionKey: ctx.requesterSessionKey ?? '',
449
+ childSessionKey: event.targetSessionKey ?? '',
450
+ targetKind: event.targetKind ?? '',
451
+ outcome: event.outcome ?? '',
452
+ success: event.outcome ? event.outcome === 'ok' : !event.error,
453
+ error: event.error ?? '',
454
+ runId: event.runId ?? ctx.runId ?? '',
392
455
  }).catch((e) => log.warn('[SafeClaw] Failed to record subagent ended:', e));
393
456
  });
394
457
  // Session lifecycle — notify service of session start (#189)
395
458
  api.on('session_start', (event, ctx) => {
396
459
  post('/session/start', {
397
- sessionId: ctx.sessionId ?? event.sessionId ?? '',
460
+ sessionId: ctx.sessionKey ?? ctx.sessionId ?? event.sessionId ?? '',
398
461
  userId: ctx.agentId ?? '',
399
462
  agentId: instanceId,
400
463
  metadata: event.metadata ?? {},
@@ -403,18 +466,23 @@ export default {
403
466
  // Session lifecycle — notify service of session end (#189)
404
467
  api.on('session_end', (event, ctx) => {
405
468
  post('/session/end', {
406
- sessionId: ctx.sessionId ?? event.sessionId ?? '',
469
+ sessionId: ctx.sessionKey ?? ctx.sessionId ?? event.sessionId ?? '',
407
470
  userId: ctx.agentId ?? '',
408
471
  agentId: instanceId,
409
472
  }).catch((e) => log.warn('[SafeClaw] Failed to record session end:', e));
410
473
  });
411
- // Inbound message governance — evaluate received messages (#190)
474
+ // Inbound message governance — evaluate received messages (#190).
475
+ // v2026.6.8 message context: sender is `event.from` (+ stable `event.senderId`),
476
+ // the session is `ctx.sessionKey`, and the channel id is on the context. The
477
+ // message context has no agentId (use accountId) and no provider field, so the
478
+ // service maps channel trust from `channelId`/`conversationId` (#313, feeds #320).
412
479
  api.on('message_received', (event, ctx) => {
413
480
  post('/evaluate/inbound-message', {
414
- sessionId: ctx.sessionId ?? event.sessionId ?? '',
415
- userId: ctx.agentId ?? '',
416
- channel: event.channel ?? ctx.channelId ?? '',
417
- sender: event.sender ?? '',
481
+ sessionId: ctx.sessionKey ?? event.sessionKey ?? '',
482
+ userId: ctx.accountId ?? '',
483
+ channel: ctx.channelId ?? '',
484
+ conversationId: ctx.conversationId ?? '',
485
+ sender: event.senderId ?? ctx.senderId ?? event.from ?? '',
418
486
  content: event.content ?? '',
419
487
  metadata: event.metadata ?? {},
420
488
  }).catch((e) => log.warn('[SafeClaw] Failed to evaluate inbound message:', e));
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, BeforeToolCallResult } from 'openclaw/plugin-sdk/core';
10
+ import type { OpenClawPluginApi, 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';
@@ -114,6 +114,16 @@ function approvalTimeoutBehaviorForRisk(riskLevel: string): 'allow' | 'deny' {
114
114
  return riskLevel === 'CriticalRisk' || riskLevel === 'HighRisk' ? 'deny' : 'allow';
115
115
  }
116
116
 
117
+ // For high-risk actions, forbid durable "allow-always" trust — the user may only
118
+ // approve this one occurrence or deny (#314). Lower-risk actions keep all options.
119
+ function approvalAllowedDecisionsForRisk(
120
+ riskLevel: string,
121
+ ): Array<'allow-once' | 'allow-always' | 'deny'> {
122
+ return riskLevel === 'CriticalRisk' || riskLevel === 'HighRisk'
123
+ ? ['allow-once', 'deny']
124
+ : ['allow-once', 'allow-always', 'deny'];
125
+ }
126
+
117
127
  // --- Plugin Definition ---
118
128
 
119
129
  let handshakeCompleted = false;
@@ -251,18 +261,26 @@ export default {
251
261
  }
252
262
 
253
263
  // THE GATE — constraint checking on every tool call (#195: use correct OpenClaw field names)
254
- api.on('before_tool_call', async (event: OpenClawPluginEvent, ctx: OpenClawPluginContext) => {
264
+ api.on('before_tool_call', async (event, ctx) => {
255
265
  const cfg = getConfig();
256
266
  if (!handshakeCompleted && cfg.failMode === 'closed' && cfg.enforcement === 'enforce') {
257
267
  return { block: true, blockReason: 'SafeClaw handshake not completed (fail-closed)' };
258
268
  }
259
269
 
260
270
  const r = await post('/evaluate/tool-call', {
261
- sessionId: ctx.sessionId ?? event.sessionId ?? '',
271
+ sessionId: ctx.sessionKey ?? ctx.sessionId ?? event.sessionId ?? '',
262
272
  userId: ctx.agentId ?? '',
263
273
  toolName: event.toolName ?? '',
264
274
  params: event.params ?? {},
265
275
  runId: ctx.runId ?? '',
276
+ // v2026.6.8 discriminators so the service can classify code-mode exec,
277
+ // file-touching envelopes, etc. (#316)
278
+ toolKind: event.toolKind ?? '',
279
+ toolInputKind: event.toolInputKind ?? '',
280
+ derivedPaths: event.derivedPaths ?? [],
281
+ // Trigger origin (#324): cron-driven runs carry ctx.jobId.
282
+ triggeredBy: ctx.jobId ? 'cron' : 'interactive',
283
+ jobId: ctx.jobId ?? '',
266
284
  });
267
285
 
268
286
  if (r === null && cfg.failMode === 'closed' && cfg.enforcement === 'enforce') {
@@ -273,18 +291,26 @@ export default {
273
291
  log.warn(`[SafeClaw] Service unavailable at ${cfg.serviceUrl} (fail-closed mode, audit-only)`);
274
292
  }
275
293
 
276
- // If service says confirmation required, use OpenClaw's native approval flow
294
+ // If service says confirmation required, use OpenClaw's native approval flow.
295
+ // Carry any param rewrite (#316) on the same result — OpenClaw applies it as
296
+ // the approval's overrideParams once the user approves, so the post-approval
297
+ // execution still runs the governed (sanitized) params.
277
298
  if (r?.confirmationRequired) {
278
299
  const riskLevel = (r.riskLevel as string) || '';
279
- return {
300
+ const result: BeforeToolCallResult = {
280
301
  requireApproval: {
281
302
  title: 'SafeClaw Governance Check',
282
303
  description: (r.reason as string) || 'This action requires confirmation',
283
304
  severity: approvalSeverityForRisk(riskLevel),
284
305
  timeoutMs: 30_000,
285
306
  timeoutBehavior: approvalTimeoutBehaviorForRisk(riskLevel),
307
+ allowedDecisions: approvalAllowedDecisionsForRisk(riskLevel),
286
308
  },
287
- } satisfies BeforeToolCallResult;
309
+ };
310
+ if (r.params && typeof r.params === 'object') {
311
+ result.params = r.params as Record<string, unknown>;
312
+ }
313
+ return result satisfies BeforeToolCallResult;
288
314
  }
289
315
 
290
316
  if (r?.block) {
@@ -297,13 +323,20 @@ export default {
297
323
  }
298
324
  // audit-only: logged server-side, no action here
299
325
  }
326
+
327
+ // Param rewrite (#316): the service only returns `params` for an allowed
328
+ // call when it sanitized the input (e.g. stripped control characters), so
329
+ // the tool executes the governed params rather than the raw ones.
330
+ if (r?.params && typeof r.params === 'object') {
331
+ return { params: r.params as Record<string, unknown> };
332
+ }
300
333
  }, { priority: 100 });
301
334
 
302
335
  // Context injection — prepend governance context to agent system prompt
303
336
  // (#195: before_agent_start is deprecated; use before_prompt_build + prependSystemContext)
304
- api.on('before_prompt_build', async (event: OpenClawPluginEvent, ctx: OpenClawPluginContext) => {
337
+ api.on('before_prompt_build', async (event, ctx) => {
305
338
  const r = await post('/context/build', {
306
- sessionId: ctx.sessionId ?? event.sessionId ?? '',
339
+ sessionId: ctx.sessionKey ?? ctx.sessionId ?? event.sessionId ?? '',
307
340
  userId: ctx.agentId ?? '',
308
341
  });
309
342
 
@@ -312,12 +345,13 @@ export default {
312
345
  }
313
346
  }, { priority: 100 });
314
347
 
315
- // Message governance — check outbound messages
316
- // (#195: use ctx.conversationId/sessionId, ctx.accountId; return only { cancel: true })
317
- api.on('message_sending', async (event: OpenClawPluginEvent, ctx: OpenClawPluginContext) => {
348
+ // Message governance — check outbound messages.
349
+ // (v2026.6.8 message context exposes sessionKey/accountId/channelId there is
350
+ // no sessionId; the event has no session field. Return only { cancel: true }.)
351
+ api.on('message_sending', async (event, ctx) => {
318
352
  const cfg = getConfig();
319
353
  const r = await post('/evaluate/message', {
320
- sessionId: ctx.conversationId ?? ctx.sessionId ?? event.sessionId ?? '',
354
+ sessionId: ctx.sessionKey ?? ctx.conversationId ?? '',
321
355
  userId: ctx.accountId ?? '',
322
356
  to: event.to,
323
357
  content: event.content,
@@ -345,31 +379,44 @@ export default {
345
379
  }
346
380
  }, { priority: 100 });
347
381
 
348
- // Async logging — fire-and-forget, no return value needed
349
- // (#195: use event.prompt for input, event.lastAssistant for output; add provider/model)
350
- api.on('llm_input', (event: OpenClawPluginEvent, ctx: OpenClawPluginContext) => {
382
+ // Async logging — fire-and-forget, no return value needed.
383
+ // (#315: v2026.6.8 raw-conversation hooks require
384
+ // `plugins.entries.safeclaw.hooks.allowConversationAccess: true` to fire at
385
+ // all — see README. Output text is `assistantTexts: string[]`, not the
386
+ // untyped `lastAssistant`. `runId` is forwarded for audit correlation.)
387
+ api.on('llm_input', (event, ctx) => {
351
388
  post('/log/llm-input', {
352
- sessionId: event.sessionId ?? ctx.sessionId ?? '',
389
+ sessionId: ctx.sessionKey ?? event.sessionId ?? ctx.sessionId ?? '',
353
390
  content: event.prompt ?? '',
354
391
  provider: event.provider ?? '',
355
392
  model: event.model ?? '',
393
+ runId: event.runId ?? ctx.runId ?? '',
356
394
  }).catch((e) => log.warn('[SafeClaw] Failed to log LLM input:', e));
357
395
  });
358
396
 
359
- api.on('llm_output', (event: OpenClawPluginEvent, ctx: OpenClawPluginContext) => {
397
+ api.on('llm_output', (event, ctx) => {
398
+ // Join assistant segments with a blank line, matching how OpenClaw v2026.6.8
399
+ // itself joins `assistantTexts`, so message boundaries are preserved in the
400
+ // audit content (joining with '' would fuse "first"+"second" into one blob).
401
+ const assistantText = Array.isArray(event.assistantTexts)
402
+ ? event.assistantTexts.join('\n\n')
403
+ : typeof event.lastAssistant === 'string'
404
+ ? event.lastAssistant
405
+ : '';
360
406
  post('/log/llm-output', {
361
- sessionId: event.sessionId ?? ctx.sessionId ?? '',
362
- content: event.lastAssistant ?? '',
407
+ sessionId: ctx.sessionKey ?? event.sessionId ?? ctx.sessionId ?? '',
408
+ content: assistantText,
363
409
  provider: event.provider ?? '',
364
410
  model: event.model ?? '',
365
411
  usage: event.usage ?? {},
412
+ runId: event.runId ?? ctx.runId ?? '',
366
413
  }).catch((e) => log.warn('[SafeClaw] Failed to log LLM output:', e));
367
414
  });
368
415
 
369
416
  // (#195: use event.toolName, !event.error for success, add durationMs and error)
370
- api.on('after_tool_call', (event: OpenClawPluginEvent, ctx: OpenClawPluginContext) => {
417
+ api.on('after_tool_call', (event, ctx) => {
371
418
  post('/record/tool-result', {
372
- sessionId: ctx.sessionId ?? event.sessionId ?? '',
419
+ sessionId: ctx.sessionKey ?? ctx.sessionId ?? event.sessionId ?? '',
373
420
  userId: ctx.agentId ?? '',
374
421
  toolName: event.toolName ?? '',
375
422
  params: event.params ?? {},
@@ -381,15 +428,27 @@ export default {
381
428
  }).catch((e) => log.warn('[SafeClaw] Failed to record tool result:', e));
382
429
  });
383
430
 
384
- // Subagent governance — block delegation bypass attempts (#188)
385
- api.on('subagent_spawning', async (event: OpenClawPluginEvent, ctx: OpenClawPluginContext) => {
431
+ // Subagent governance — block delegation bypass attempts (#188).
432
+ // NOTE: `subagent_spawning` is a deprecated OpenClaw compatibility hook
433
+ // (removal scheduled after 2026-08-16). In v2026.6.8 the context exposes only
434
+ // session keys (`requesterSessionKey`/`childSessionKey`/`runId`) — there is NO
435
+ // parent agentId at this hook. The event carries the *child's* `agentId`. So
436
+ // we forward the parent/child session keys and the child agent id; the service
437
+ // cannot do the killed-PARENT lookup from a session key alone, so spawn-time
438
+ // delegation enforcement is reworked onto a session-keyed hierarchy in #321.
439
+ // `requester` is an object (channel/account/to/threadId), not a string. (#312)
440
+ api.on('subagent_spawning', async (event, ctx) => {
386
441
  const cfg = getConfig();
387
442
  const r = await post('/evaluate/subagent-spawn', {
388
- sessionId: ctx.sessionId ?? event.sessionId ?? '',
389
- userId: ctx.agentId ?? '',
390
- parentAgentId: event.parentAgentId,
391
- childConfig: event.childConfig ?? {},
392
- reason: event.reason ?? '',
443
+ sessionId: ctx.requesterSessionKey ?? ctx.childSessionKey ?? '',
444
+ parentSessionKey: ctx.requesterSessionKey ?? '',
445
+ childSessionKey: ctx.childSessionKey ?? event.childSessionKey ?? '',
446
+ childAgentId: event.agentId ?? '',
447
+ mode: event.mode ?? '',
448
+ threadRequested: event.threadRequested ?? false,
449
+ requester: event.requester ?? {},
450
+ runId: ctx.runId ?? '',
451
+ reason: event.label ?? '',
393
452
  });
394
453
 
395
454
  // Fail-closed handling (matches before_tool_call / message_sending pattern)
@@ -409,19 +468,27 @@ export default {
409
468
  }
410
469
  }, { priority: 100 });
411
470
 
412
- // Subagent ended — record child agent lifecycle (#188)
413
- api.on('subagent_ended', (event: OpenClawPluginEvent, ctx: OpenClawPluginContext) => {
471
+ // Subagent ended — record child agent lifecycle (#188).
472
+ // v2026.6.8 reports `targetSessionKey`/`targetKind`/`outcome`/`error`/`runId`,
473
+ // not parent/child agent IDs. The ended child is `targetSessionKey`; the
474
+ // parent is the requester session key from the subagent context. (#312)
475
+ api.on('subagent_ended', (event, ctx) => {
414
476
  post('/record/subagent-ended', {
415
- sessionId: ctx.sessionId ?? event.sessionId ?? '',
416
- parentAgentId: event.parentAgentId,
417
- childAgentId: event.childAgentId,
477
+ sessionId: ctx.requesterSessionKey ?? event.targetSessionKey ?? '',
478
+ parentSessionKey: ctx.requesterSessionKey ?? '',
479
+ childSessionKey: event.targetSessionKey ?? '',
480
+ targetKind: event.targetKind ?? '',
481
+ outcome: event.outcome ?? '',
482
+ success: event.outcome ? event.outcome === 'ok' : !event.error,
483
+ error: event.error ?? '',
484
+ runId: event.runId ?? ctx.runId ?? '',
418
485
  }).catch((e) => log.warn('[SafeClaw] Failed to record subagent ended:', e));
419
486
  });
420
487
 
421
488
  // Session lifecycle — notify service of session start (#189)
422
- api.on('session_start', (event: OpenClawPluginEvent, ctx: OpenClawPluginContext) => {
489
+ api.on('session_start', (event, ctx) => {
423
490
  post('/session/start', {
424
- sessionId: ctx.sessionId ?? event.sessionId ?? '',
491
+ sessionId: ctx.sessionKey ?? ctx.sessionId ?? event.sessionId ?? '',
425
492
  userId: ctx.agentId ?? '',
426
493
  agentId: instanceId,
427
494
  metadata: event.metadata ?? {},
@@ -429,21 +496,26 @@ export default {
429
496
  });
430
497
 
431
498
  // Session lifecycle — notify service of session end (#189)
432
- api.on('session_end', (event: OpenClawPluginEvent, ctx: OpenClawPluginContext) => {
499
+ api.on('session_end', (event, ctx) => {
433
500
  post('/session/end', {
434
- sessionId: ctx.sessionId ?? event.sessionId ?? '',
501
+ sessionId: ctx.sessionKey ?? ctx.sessionId ?? event.sessionId ?? '',
435
502
  userId: ctx.agentId ?? '',
436
503
  agentId: instanceId,
437
504
  }).catch((e) => log.warn('[SafeClaw] Failed to record session end:', e));
438
505
  });
439
506
 
440
- // Inbound message governance — evaluate received messages (#190)
441
- api.on('message_received', (event: OpenClawPluginEvent, ctx: OpenClawPluginContext) => {
507
+ // Inbound message governance — evaluate received messages (#190).
508
+ // v2026.6.8 message context: sender is `event.from` (+ stable `event.senderId`),
509
+ // the session is `ctx.sessionKey`, and the channel id is on the context. The
510
+ // message context has no agentId (use accountId) and no provider field, so the
511
+ // service maps channel trust from `channelId`/`conversationId` (#313, feeds #320).
512
+ api.on('message_received', (event, ctx) => {
442
513
  post('/evaluate/inbound-message', {
443
- sessionId: ctx.sessionId ?? event.sessionId ?? '',
444
- userId: ctx.agentId ?? '',
445
- channel: event.channel ?? (ctx as any).channelId ?? '',
446
- sender: event.sender ?? '',
514
+ sessionId: ctx.sessionKey ?? event.sessionKey ?? '',
515
+ userId: ctx.accountId ?? '',
516
+ channel: ctx.channelId ?? '',
517
+ conversationId: ctx.conversationId ?? '',
518
+ sender: event.senderId ?? ctx.senderId ?? event.from ?? '',
447
519
  content: event.content ?? '',
448
520
  metadata: event.metadata ?? {},
449
521
  }).catch((e) => log.warn('[SafeClaw] Failed to evaluate inbound message:', e));
@@ -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.0",
5
+ "version": "2.0.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.5.0",
3
+ "version": "2.0.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",
@@ -58,7 +58,7 @@
58
58
  "typescript": "^5.4.0"
59
59
  },
60
60
  "peerDependencies": {
61
- "openclaw": ">=2026.1.0"
61
+ "openclaw": ">=2026.6.8"
62
62
  },
63
63
  "peerDependenciesMeta": {
64
64
  "openclaw": {
@@ -1,104 +1,316 @@
1
1
  /**
2
- * Ambient type declarations for OpenClaw Plugin SDK.
3
- * These match the types exported by openclaw/plugin-sdk as of v2026.4.
4
- * When installed inside OpenClaw, the real SDK types take precedence.
2
+ * Ambient type declarations for the OpenClaw Plugin SDK.
3
+ * Transcribed from the published `openclaw@2026.6.8` source
4
+ * (`src/plugins/hook-types.ts`, `hook-message.types.ts`,
5
+ * `hook-before-tool-call-result.ts`). When installed inside OpenClaw the real
6
+ * SDK types take precedence.
7
+ *
8
+ * Design note (#317): each registered hook has BOTH a precise event payload and
9
+ * a precise *context* type — the contexts genuinely differ (agent runs expose
10
+ * `agentId`/`sessionId`; message hooks expose neither, only `sessionKey`;
11
+ * subagent hooks expose only `requesterSessionKey`/`childSessionKey`/`runId`).
12
+ * `on()` is typed via per-hook overloads keyed off `PluginHookEventMap` /
13
+ * `PluginHookContextMap`, so reading a field that does not exist on that hook's
14
+ * real payload/context (e.g. `ctx.agentId` on a subagent hook, `ctx.sessionId`
15
+ * on a message hook) fails `tsc` instead of silently reading `undefined`.
5
16
  */
6
17
 
7
18
  declare module 'openclaw/plugin-sdk/core' {
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
- */
15
- on(
16
- hookName: string,
17
- handler: (event: OpenClawPluginEvent, ctx: OpenClawPluginContext) => Promise<Record<string, unknown> | void> | void,
18
- options?: { priority?: number },
19
- ): void;
20
- registerService?(service: OpenClawPluginService): void;
21
- registerCli?(registrar: (ctx: { program: unknown; config: unknown }) => void, opts?: { commands: string[] }): void;
22
- registerTool?(tool: OpenClawPluginTool): void;
23
- registerCommand?(def: { name: string; description: string; execute: (ctx: unknown) => Promise<string | void> }): void;
24
- pluginConfig?: Record<string, unknown>;
25
- logger?: OpenClawPluginLogger;
19
+ // ---- Per-hook contexts (v2026.6.8) ----
20
+
21
+ /** PluginHookAgentContext agent-runtime hooks (tool calls, prompts, llm, session). */
22
+ export interface PluginHookAgentContext {
23
+ runId?: string;
24
+ jobId?: string;
25
+ agentId?: string;
26
+ /** Canonical conversation key — present on agent-runtime hooks. */
27
+ sessionKey?: string;
28
+ sessionId?: string;
29
+ workspaceDir?: string;
30
+ modelProviderId?: string;
31
+ modelId?: string;
32
+ messageProvider?: string;
33
+ /** Channel/plugin id for channel-originated runs, e.g. "discord". */
34
+ channel?: string;
35
+ chatId?: string;
36
+ senderId?: string;
37
+ trigger?: string;
38
+ channelId?: string;
26
39
  }
27
40
 
28
- export interface OpenClawPluginEvent {
41
+ /** PluginHookMessageContext inbound/outbound message hooks. No agentId / sessionId. */
42
+ export interface PluginHookMessageContext {
43
+ channelId: string;
44
+ accountId?: string;
45
+ conversationId?: string;
46
+ /** Canonical conversation key; the message-hook equivalent of sessionId. */
47
+ sessionKey?: string;
48
+ runId?: string;
49
+ messageId?: string;
50
+ senderId?: string;
51
+ replyToId?: string;
52
+ }
53
+
54
+ /** PluginHookSubagentContext — subagent_spawning / subagent_ended. Session keys only. */
55
+ export interface PluginHookSubagentContext {
56
+ runId?: string;
57
+ childSessionKey?: string;
58
+ /** The requesting (parent) session key. There is no parent agentId here. */
59
+ requesterSessionKey?: string;
60
+ }
61
+
62
+ /** Loose fallback context for hooks SafeClaw does not strongly type. */
63
+ export interface OpenClawPluginContext {
64
+ [key: string]: unknown;
65
+ }
66
+
67
+ // ---- Per-hook event payloads (v2026.6.8) ----
68
+
69
+ export interface BeforeToolCallEvent {
70
+ toolName?: string;
71
+ params?: Record<string, unknown>;
29
72
  sessionId?: string;
73
+ runId?: string;
74
+ toolCallId?: string;
75
+ /** e.g. "code_mode_exec" — distinguishes code-mode exec cells from shell exec. */
76
+ toolKind?: string;
77
+ /** Code-mode body language, e.g. "javascript" | "typescript". */
78
+ toolInputKind?: string;
79
+ /** Host-derived target paths for file-touching envelopes (e.g. apply_patch). */
80
+ derivedPaths?: readonly string[];
81
+ }
82
+
83
+ export interface AfterToolCallEvent {
30
84
  toolName?: string;
31
85
  params?: Record<string, unknown>;
32
- to?: string;
33
- content?: string;
34
86
  result?: unknown;
35
87
  error?: string;
36
88
  durationMs?: number;
89
+ sessionId?: string;
90
+ runId?: string;
91
+ }
92
+
93
+ export interface BeforePromptBuildEvent {
94
+ sessionId?: string;
95
+ }
96
+
97
+ /** v2026.6.8 PluginHookMessageSendingEvent — no sessionId on the event. */
98
+ export interface MessageSendingEvent {
99
+ to: string;
100
+ content: string;
101
+ replyToId?: string | number;
102
+ threadId?: string | number;
103
+ metadata?: Record<string, unknown>;
104
+ }
105
+
106
+ /** v2026.6.8 PluginHookMessageReceivedEvent — sender is `from`; key is `sessionKey`. */
107
+ export interface MessageReceivedEvent {
108
+ from: string;
109
+ content: string;
110
+ timestamp?: number;
111
+ threadId?: string | number;
112
+ messageId?: string;
113
+ senderId?: string;
114
+ replyToId?: string;
115
+ replyToSender?: string;
116
+ sessionKey?: string;
117
+ runId?: string;
118
+ metadata?: Record<string, unknown>;
119
+ }
120
+
121
+ export interface LlmInputEvent {
122
+ runId?: string;
123
+ sessionId?: string;
124
+ provider?: string;
125
+ model?: string;
126
+ systemPrompt?: string;
37
127
  prompt?: string;
128
+ }
129
+
130
+ /** v2026.6.8: reliable text is `assistantTexts: string[]`; `lastAssistant` is untyped. */
131
+ export interface LlmOutputEvent {
132
+ runId?: string;
133
+ sessionId?: string;
38
134
  provider?: string;
39
135
  model?: string;
40
- lastAssistant?: string;
136
+ prompt?: string;
137
+ assistantTexts?: string[];
138
+ lastAssistant?: unknown;
41
139
  usage?: Record<string, unknown>;
140
+ }
141
+
142
+ /**
143
+ * Deprecated compatibility hook (removal scheduled after 2026-08-16).
144
+ * v2026.6.8 PluginHookSubagentSpawnBase — `requester` is an object, not a string.
145
+ */
146
+ export interface SubagentSpawningEvent {
147
+ childSessionKey: string;
148
+ agentId: string;
149
+ label?: string;
150
+ mode: 'run' | 'session';
151
+ requester?: {
152
+ channel?: string;
153
+ accountId?: string;
154
+ to?: string;
155
+ threadId?: string | number;
156
+ };
157
+ threadRequested: boolean;
158
+ }
159
+
160
+ export interface SubagentEndedEvent {
161
+ targetSessionKey: string;
162
+ targetKind: 'subagent' | 'acp';
163
+ reason: string;
164
+ sendFarewell?: boolean;
165
+ accountId?: string;
42
166
  runId?: string;
43
- toolCallId?: string;
44
- childAgentId?: string;
45
- parentAgentId?: string;
46
- childConfig?: Record<string, unknown>;
47
- reason?: string;
48
- metadata?: Record<string, unknown>;
49
- channel?: string;
50
- sender?: string;
51
- [key: string]: unknown;
167
+ endedAt?: number;
168
+ outcome?: 'ok' | 'error' | 'timeout' | 'killed' | 'reset' | 'deleted';
169
+ error?: string;
52
170
  }
53
171
 
54
- export interface OpenClawPluginContext {
172
+ export interface SessionLifecycleEvent {
55
173
  sessionId?: string;
56
- agentId?: string;
57
- runId?: string;
58
- conversationId?: string;
59
- accountId?: string;
60
- channelId?: string;
61
- workspaceDir?: string;
174
+ sessionKey?: string;
175
+ metadata?: Record<string, unknown>;
176
+ }
177
+
178
+ /** Generic fallback payload for hooks SafeClaw does not strongly type. */
179
+ export interface OpenClawPluginEvent {
62
180
  [key: string]: unknown;
63
181
  }
64
182
 
65
- export interface OpenClawPluginService {
66
- id: string;
67
- start: () => void | Promise<void>;
68
- stop?: () => void | Promise<void>;
183
+ export interface PluginHookEventMap {
184
+ before_tool_call: BeforeToolCallEvent;
185
+ after_tool_call: AfterToolCallEvent;
186
+ before_prompt_build: BeforePromptBuildEvent;
187
+ message_sending: MessageSendingEvent;
188
+ message_received: MessageReceivedEvent;
189
+ llm_input: LlmInputEvent;
190
+ llm_output: LlmOutputEvent;
191
+ subagent_spawning: SubagentSpawningEvent;
192
+ subagent_ended: SubagentEndedEvent;
193
+ session_start: SessionLifecycleEvent;
194
+ session_end: SessionLifecycleEvent;
69
195
  }
70
196
 
71
- export interface OpenClawPluginTool {
72
- name: string;
73
- description: string;
74
- parameters: Record<string, unknown>;
75
- execute: (params: Record<string, unknown>, ctx: Record<string, unknown>) => Promise<unknown>;
197
+ export interface PluginHookContextMap {
198
+ before_tool_call: PluginHookAgentContext;
199
+ after_tool_call: PluginHookAgentContext;
200
+ before_prompt_build: PluginHookAgentContext;
201
+ message_sending: PluginHookMessageContext;
202
+ message_received: PluginHookMessageContext;
203
+ llm_input: PluginHookAgentContext;
204
+ llm_output: PluginHookAgentContext;
205
+ subagent_spawning: PluginHookSubagentContext;
206
+ subagent_ended: PluginHookSubagentContext;
207
+ session_start: PluginHookAgentContext;
208
+ session_end: PluginHookAgentContext;
76
209
  }
77
210
 
211
+ // ---- Hook results ----
212
+
213
+ export type PluginApprovalDecision = 'allow-once' | 'allow-always' | 'deny';
214
+ /** v2026.6.8: typed resolution union (was a free-form string). */
215
+ export type PluginApprovalResolution = PluginApprovalDecision | 'timeout' | 'cancelled';
216
+
78
217
  export interface PluginApprovalRequest {
79
218
  title: string;
80
219
  description: string;
81
220
  severity?: 'info' | 'warning' | 'critical';
82
221
  timeoutMs?: number;
83
222
  timeoutBehavior?: 'allow' | 'deny';
223
+ /** v2026.6.8: restrict the offered decisions (e.g. forbid durable "allow-always"). */
224
+ allowedDecisions?: PluginApprovalDecision[];
84
225
  pluginId?: string;
85
226
  onResolution?: (decision: PluginApprovalResolution) => Promise<void> | void;
86
227
  }
87
228
 
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
229
  export interface BeforeToolCallResult {
99
230
  block?: boolean;
100
231
  blockReason?: string;
101
232
  requireApproval?: PluginApprovalRequest;
233
+ /** v2026.6.8: rewrite tool params before execution. */
234
+ params?: Record<string, unknown>;
235
+ }
236
+
237
+ export interface BeforePromptBuildResult {
238
+ prependSystemContext?: string;
239
+ }
240
+
241
+ export interface MessageSendingResult {
242
+ content?: string;
243
+ cancel?: boolean;
244
+ cancelReason?: string;
245
+ }
246
+
247
+ /** v2026.6.8 discriminated union — the error variant requires `error: string`. */
248
+ export type SubagentSpawningResult =
249
+ | {
250
+ status: 'ok';
251
+ threadBindingReady?: boolean;
252
+ deliveryOrigin?: {
253
+ channel?: string;
254
+ accountId?: string;
255
+ to?: string;
256
+ threadId?: string | number;
257
+ };
258
+ }
259
+ | { status: 'error'; error: string };
260
+
261
+ export interface PluginHookResultMap {
262
+ before_tool_call: BeforeToolCallResult;
263
+ after_tool_call: void;
264
+ before_prompt_build: BeforePromptBuildResult;
265
+ message_sending: MessageSendingResult;
266
+ message_received: void;
267
+ llm_input: void;
268
+ llm_output: void;
269
+ subagent_spawning: SubagentSpawningResult;
270
+ subagent_ended: void;
271
+ session_start: void;
272
+ session_end: void;
273
+ }
274
+
275
+ export interface OpenClawPluginApi {
276
+ /** Typed overload for the hooks SafeClaw registers (precise event + context). */
277
+ on<K extends keyof PluginHookEventMap>(
278
+ hookName: K,
279
+ handler: (
280
+ event: PluginHookEventMap[K],
281
+ ctx: PluginHookContextMap[K],
282
+ ) => PluginHookResultMap[K] | void | Promise<PluginHookResultMap[K] | void>,
283
+ options?: { priority?: number; timeoutMs?: number },
284
+ ): void;
285
+ /** Generic fallback for any other hook. */
286
+ on(
287
+ hookName: string,
288
+ handler: (
289
+ event: OpenClawPluginEvent,
290
+ ctx: OpenClawPluginContext,
291
+ ) => Promise<Record<string, unknown> | void> | void,
292
+ options?: { priority?: number; timeoutMs?: number },
293
+ ): void;
294
+
295
+ registerService?(service: OpenClawPluginService): void;
296
+ registerCli?(registrar: (ctx: { program: unknown; config: unknown }) => void, opts?: { commands: string[] }): void;
297
+ registerTool?(tool: OpenClawPluginTool): void;
298
+ registerCommand?(def: { name: string; description: string; execute: (ctx: unknown) => Promise<string | void> }): void;
299
+ pluginConfig?: Record<string, unknown>;
300
+ logger?: OpenClawPluginLogger;
301
+ }
302
+
303
+ export interface OpenClawPluginService {
304
+ id: string;
305
+ start: () => void | Promise<void>;
306
+ stop?: () => void | Promise<void>;
307
+ }
308
+
309
+ export interface OpenClawPluginTool {
310
+ name: string;
311
+ description: string;
312
+ parameters: Record<string, unknown>;
313
+ execute: (params: Record<string, unknown>, ctx: Record<string, unknown>) => Promise<unknown>;
102
314
  }
103
315
 
104
316
  export interface OpenClawPluginLogger {