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 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: configHash(getConfig()),
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
- throw new Error('SafeClaw service unavailable (fail-closed mode)');
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
- throw new Error(r.reason || 'Blocked by SafeClaw: delegation bypass detected');
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) {
@@ -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: configHash(getConfig()),
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
- throw new Error('SafeClaw service unavailable (fail-closed mode)');
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
- throw new Error((r.reason as string) || 'Blocked by SafeClaw: delegation bypass detected');
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)
@@ -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.3.0",
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.4.0",
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
  }
@@ -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.
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;