openclaw-safeclaw-plugin 1.3.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/cli.tsx +31 -29
- package/dist/cli.js +30 -28
- package/dist/index.js +42 -21
- package/dist/tui/Status.js +1 -1
- package/dist/tui/config.js +20 -6
- package/index.ts +46 -23
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/tui/Status.tsx +1 -1
- package/tui/config.ts +15 -5
- package/types/openclaw-sdk.d.ts +36 -1
package/cli.tsx
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { render } from 'ink';
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync, lstatSync, unlinkSync
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync, lstatSync, unlinkSync } from 'fs';
|
|
6
6
|
import { join, dirname } from 'path';
|
|
7
7
|
import { homedir } from 'os';
|
|
8
8
|
import { fileURLToPath } from 'url';
|
|
@@ -94,7 +94,32 @@ const command = args[0];
|
|
|
94
94
|
|
|
95
95
|
// Handle --help / -h for any command position
|
|
96
96
|
if (!command || command === '--help' || command === '-h' || command === 'help') {
|
|
97
|
-
|
|
97
|
+
console.log('safeclaw-plugin — OpenClaw plugin CLI for SafeClaw governance');
|
|
98
|
+
console.log('');
|
|
99
|
+
console.log('Usage: safeclaw-plugin <command> [options]');
|
|
100
|
+
console.log('');
|
|
101
|
+
console.log('Setup:');
|
|
102
|
+
console.log(' connect <api-key> Save API key, validate via handshake, register with OpenClaw');
|
|
103
|
+
console.log(' Keys start with "sc_". Get yours at https://safeclaw.eu/dashboard');
|
|
104
|
+
console.log(' setup Register plugin with OpenClaw without an API key (manual setup)');
|
|
105
|
+
console.log(' restart-openclaw Restart the OpenClaw daemon to pick up plugin changes');
|
|
106
|
+
console.log('');
|
|
107
|
+
console.log('Diagnostics:');
|
|
108
|
+
console.log(' status Run 9 checks: config, API key, service health, evaluate endpoint,');
|
|
109
|
+
console.log(' handshake, OpenClaw binary, plugin files, OpenClaw config, NemoClaw');
|
|
110
|
+
console.log('');
|
|
111
|
+
console.log('Configuration:');
|
|
112
|
+
console.log(' config show Show current enforcement, failMode, enabled, serviceUrl, apiKey');
|
|
113
|
+
console.log(' config set <k> <v> Set a config value. Keys: enforcement, failMode, enabled, serviceUrl');
|
|
114
|
+
console.log(' enforcement: enforce | warn-only | audit-only | disabled');
|
|
115
|
+
console.log(' failMode: open (allow on error) | closed (block on error)');
|
|
116
|
+
console.log(' enabled: true | false');
|
|
117
|
+
console.log('');
|
|
118
|
+
console.log('Interactive:');
|
|
119
|
+
console.log(' tui Open the interactive settings TUI (Status, Settings, About tabs)');
|
|
120
|
+
console.log('');
|
|
121
|
+
console.log('For the service CLI (serve, audit, policy, pref), use the "safeclaw" command.');
|
|
122
|
+
process.exit(0);
|
|
98
123
|
} else if (command === 'connect') {
|
|
99
124
|
const apiKey = args[1];
|
|
100
125
|
const serviceUrlIdx = args.indexOf('--service-url');
|
|
@@ -192,7 +217,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
|
|
|
192
217
|
console.log(`enforcement: ${cfg.enforcement}`);
|
|
193
218
|
console.log(`failMode: ${cfg.failMode}`);
|
|
194
219
|
console.log(`serviceUrl: ${cfg.serviceUrl}`);
|
|
195
|
-
console.log(`apiKey: ${cfg.apiKey ? `${cfg.apiKey.slice(0, 6)}
|
|
220
|
+
console.log(`apiKey: ${cfg.apiKey ? `${cfg.apiKey.slice(0, 6)}..${cfg.apiKey.slice(-4)}` : '(not set)'}`);
|
|
196
221
|
console.log(`timeoutMs: ${cfg.timeoutMs}`);
|
|
197
222
|
} else if (subcommand === 'set') {
|
|
198
223
|
const key = args[2];
|
|
@@ -479,30 +504,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
|
|
|
479
504
|
console.log('Some checks failed. Fix the issues above.');
|
|
480
505
|
}
|
|
481
506
|
} else {
|
|
482
|
-
console.
|
|
483
|
-
console.
|
|
484
|
-
|
|
485
|
-
console.log('');
|
|
486
|
-
console.log('Setup:');
|
|
487
|
-
console.log(' connect <api-key> Save API key, validate via handshake, register with OpenClaw');
|
|
488
|
-
console.log(' Keys start with "sc_". Get yours at https://safeclaw.eu/dashboard');
|
|
489
|
-
console.log(' setup Register plugin with OpenClaw without an API key (manual setup)');
|
|
490
|
-
console.log(' restart-openclaw Restart the OpenClaw daemon to pick up plugin changes');
|
|
491
|
-
console.log('');
|
|
492
|
-
console.log('Diagnostics:');
|
|
493
|
-
console.log(' status Run 9 checks: config, API key, service health, evaluate endpoint,');
|
|
494
|
-
console.log(' handshake, OpenClaw binary, plugin files, OpenClaw config, NemoClaw');
|
|
495
|
-
console.log('');
|
|
496
|
-
console.log('Configuration:');
|
|
497
|
-
console.log(' config show Show current enforcement, failMode, enabled, serviceUrl, apiKey');
|
|
498
|
-
console.log(' config set <k> <v> Set a config value. Keys: enforcement, failMode, enabled, serviceUrl');
|
|
499
|
-
console.log(' enforcement: enforce | warn-only | audit-only | disabled');
|
|
500
|
-
console.log(' failMode: open (allow on error) | closed (block on error)');
|
|
501
|
-
console.log(' enabled: true | false');
|
|
502
|
-
console.log('');
|
|
503
|
-
console.log('Interactive:');
|
|
504
|
-
console.log(' tui Open the interactive settings TUI (Status, Settings, About tabs)');
|
|
505
|
-
console.log('');
|
|
506
|
-
console.log('For the service CLI (serve, audit, policy, pref), use the "safeclaw" command.');
|
|
507
|
-
process.exit(0);
|
|
507
|
+
console.error(`Unknown command: "${command}"`);
|
|
508
|
+
console.error('Run "safeclaw-plugin --help" for usage information.');
|
|
509
|
+
process.exit(1);
|
|
508
510
|
}
|
package/dist/cli.js
CHANGED
|
@@ -85,7 +85,32 @@ const args = process.argv.slice(2);
|
|
|
85
85
|
const command = args[0];
|
|
86
86
|
// Handle --help / -h for any command position
|
|
87
87
|
if (!command || command === '--help' || command === '-h' || command === 'help') {
|
|
88
|
-
|
|
88
|
+
console.log('safeclaw-plugin — OpenClaw plugin CLI for SafeClaw governance');
|
|
89
|
+
console.log('');
|
|
90
|
+
console.log('Usage: safeclaw-plugin <command> [options]');
|
|
91
|
+
console.log('');
|
|
92
|
+
console.log('Setup:');
|
|
93
|
+
console.log(' connect <api-key> Save API key, validate via handshake, register with OpenClaw');
|
|
94
|
+
console.log(' Keys start with "sc_". Get yours at https://safeclaw.eu/dashboard');
|
|
95
|
+
console.log(' setup Register plugin with OpenClaw without an API key (manual setup)');
|
|
96
|
+
console.log(' restart-openclaw Restart the OpenClaw daemon to pick up plugin changes');
|
|
97
|
+
console.log('');
|
|
98
|
+
console.log('Diagnostics:');
|
|
99
|
+
console.log(' status Run 9 checks: config, API key, service health, evaluate endpoint,');
|
|
100
|
+
console.log(' handshake, OpenClaw binary, plugin files, OpenClaw config, NemoClaw');
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log('Configuration:');
|
|
103
|
+
console.log(' config show Show current enforcement, failMode, enabled, serviceUrl, apiKey');
|
|
104
|
+
console.log(' config set <k> <v> Set a config value. Keys: enforcement, failMode, enabled, serviceUrl');
|
|
105
|
+
console.log(' enforcement: enforce | warn-only | audit-only | disabled');
|
|
106
|
+
console.log(' failMode: open (allow on error) | closed (block on error)');
|
|
107
|
+
console.log(' enabled: true | false');
|
|
108
|
+
console.log('');
|
|
109
|
+
console.log('Interactive:');
|
|
110
|
+
console.log(' tui Open the interactive settings TUI (Status, Settings, About tabs)');
|
|
111
|
+
console.log('');
|
|
112
|
+
console.log('For the service CLI (serve, audit, policy, pref), use the "safeclaw" command.');
|
|
113
|
+
process.exit(0);
|
|
89
114
|
}
|
|
90
115
|
else if (command === 'connect') {
|
|
91
116
|
const apiKey = args[1];
|
|
@@ -180,7 +205,7 @@ else if (command === 'config') {
|
|
|
180
205
|
console.log(`enforcement: ${cfg.enforcement}`);
|
|
181
206
|
console.log(`failMode: ${cfg.failMode}`);
|
|
182
207
|
console.log(`serviceUrl: ${cfg.serviceUrl}`);
|
|
183
|
-
console.log(`apiKey: ${cfg.apiKey ? `${cfg.apiKey.slice(0, 6)}
|
|
208
|
+
console.log(`apiKey: ${cfg.apiKey ? `${cfg.apiKey.slice(0, 6)}..${cfg.apiKey.slice(-4)}` : '(not set)'}`);
|
|
184
209
|
console.log(`timeoutMs: ${cfg.timeoutMs}`);
|
|
185
210
|
}
|
|
186
211
|
else if (subcommand === 'set') {
|
|
@@ -490,30 +515,7 @@ else if (command === 'status') {
|
|
|
490
515
|
}
|
|
491
516
|
}
|
|
492
517
|
else {
|
|
493
|
-
console.
|
|
494
|
-
console.
|
|
495
|
-
|
|
496
|
-
console.log('');
|
|
497
|
-
console.log('Setup:');
|
|
498
|
-
console.log(' connect <api-key> Save API key, validate via handshake, register with OpenClaw');
|
|
499
|
-
console.log(' Keys start with "sc_". Get yours at https://safeclaw.eu/dashboard');
|
|
500
|
-
console.log(' setup Register plugin with OpenClaw without an API key (manual setup)');
|
|
501
|
-
console.log(' restart-openclaw Restart the OpenClaw daemon to pick up plugin changes');
|
|
502
|
-
console.log('');
|
|
503
|
-
console.log('Diagnostics:');
|
|
504
|
-
console.log(' status Run 9 checks: config, API key, service health, evaluate endpoint,');
|
|
505
|
-
console.log(' handshake, OpenClaw binary, plugin files, OpenClaw config, NemoClaw');
|
|
506
|
-
console.log('');
|
|
507
|
-
console.log('Configuration:');
|
|
508
|
-
console.log(' config show Show current enforcement, failMode, enabled, serviceUrl, apiKey');
|
|
509
|
-
console.log(' config set <k> <v> Set a config value. Keys: enforcement, failMode, enabled, serviceUrl');
|
|
510
|
-
console.log(' enforcement: enforce | warn-only | audit-only | disabled');
|
|
511
|
-
console.log(' failMode: open (allow on error) | closed (block on error)');
|
|
512
|
-
console.log(' enabled: true | false');
|
|
513
|
-
console.log('');
|
|
514
|
-
console.log('Interactive:');
|
|
515
|
-
console.log(' tui Open the interactive settings TUI (Status, Settings, About tabs)');
|
|
516
|
-
console.log('');
|
|
517
|
-
console.log('For the service CLI (serve, audit, policy, pref), use the "safeclaw" command.');
|
|
518
|
-
process.exit(0);
|
|
518
|
+
console.error(`Unknown command: "${command}"`);
|
|
519
|
+
console.error('Run "safeclaw-plugin --help" for usage information.');
|
|
520
|
+
process.exit(1);
|
|
519
521
|
}
|
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() {
|
|
@@ -149,20 +151,26 @@ export default {
|
|
|
149
151
|
name: 'SafeClaw Neurosymbolic Governance',
|
|
150
152
|
version: PLUGIN_VERSION,
|
|
151
153
|
register(api) {
|
|
154
|
+
_ocPluginConfig = api.pluginConfig ?? {};
|
|
155
|
+
log = api.logger ?? console;
|
|
152
156
|
if (!getConfig().enabled) {
|
|
153
|
-
|
|
157
|
+
log.info('[SafeClaw] Plugin disabled');
|
|
154
158
|
return;
|
|
155
159
|
}
|
|
156
|
-
log = api.logger ?? console;
|
|
157
|
-
_ocPluginConfig = api.pluginConfig ?? {};
|
|
158
160
|
// Generate a unique instance ID for this plugin run (fallback when agentId is not configured)
|
|
159
161
|
const instanceId = getConfig().agentId || `instance-${crypto.randomUUID()}`;
|
|
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/Status.js
CHANGED
|
@@ -129,7 +129,7 @@ export default function Status({ config }) {
|
|
|
129
129
|
checkOpenClaw();
|
|
130
130
|
}, 10000);
|
|
131
131
|
return () => clearInterval(interval);
|
|
132
|
-
}, []);
|
|
132
|
+
}, [config.serviceUrl, config.apiKey]);
|
|
133
133
|
useInput((input) => {
|
|
134
134
|
if (input === 'r') {
|
|
135
135
|
restartOpenClaw();
|
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';
|
|
@@ -33,8 +33,11 @@ export function loadConfig() {
|
|
|
33
33
|
defaults.serviceUrl = raw.remote.serviceUrl;
|
|
34
34
|
if (raw.remote?.apiKey)
|
|
35
35
|
defaults.apiKey = raw.remote.apiKey;
|
|
36
|
-
if (raw.remote?.timeoutMs)
|
|
37
|
-
|
|
36
|
+
if (raw.remote?.timeoutMs != null) {
|
|
37
|
+
const t = Number(raw.remote.timeoutMs);
|
|
38
|
+
if (Number.isFinite(t) && t > 0)
|
|
39
|
+
defaults.timeoutMs = t;
|
|
40
|
+
}
|
|
38
41
|
if (raw.enforcement?.mode)
|
|
39
42
|
defaults.enforcement = raw.enforcement.mode;
|
|
40
43
|
if (raw.enforcement?.failMode)
|
|
@@ -67,6 +70,8 @@ export function loadConfig() {
|
|
|
67
70
|
}
|
|
68
71
|
if (process.env.SAFECLAW_ENABLED === 'false')
|
|
69
72
|
defaults.enabled = false;
|
|
73
|
+
else if (process.env.SAFECLAW_ENABLED === 'true')
|
|
74
|
+
defaults.enabled = true;
|
|
70
75
|
const enforcement = process.env.SAFECLAW_ENFORCEMENT;
|
|
71
76
|
if (enforcement && ['enforce', 'warn-only', 'audit-only', 'disabled'].includes(enforcement)) {
|
|
72
77
|
defaults.enforcement = enforcement;
|
|
@@ -140,6 +145,7 @@ export function saveConfig(config) {
|
|
|
140
145
|
existing.remote = {};
|
|
141
146
|
}
|
|
142
147
|
existing.remote.serviceUrl = config.serviceUrl;
|
|
148
|
+
existing.remote.timeoutMs = config.timeoutMs;
|
|
143
149
|
if (config.apiKey) {
|
|
144
150
|
existing.remote.apiKey = config.apiKey;
|
|
145
151
|
}
|
|
@@ -161,11 +167,18 @@ export function saveConfig(config) {
|
|
|
161
167
|
mkdirSync(dirname(CONFIG_PATH), { recursive: true, mode: 0o700 });
|
|
162
168
|
try {
|
|
163
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 */ }
|
|
164
174
|
}
|
|
165
175
|
catch (e) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
176
|
+
const code = e.code;
|
|
177
|
+
if (code === 'EROFS') {
|
|
178
|
+
throw new Error('Cannot save config: filesystem is read-only (sandbox environment)');
|
|
179
|
+
}
|
|
180
|
+
if (code === 'EACCES') {
|
|
181
|
+
throw new Error(`Cannot save config: permission denied for ${CONFIG_PATH}`);
|
|
169
182
|
}
|
|
170
183
|
throw e;
|
|
171
184
|
}
|
|
@@ -180,6 +193,7 @@ export function configHash(config) {
|
|
|
180
193
|
enforcement: config.enforcement,
|
|
181
194
|
failMode: config.failMode,
|
|
182
195
|
serviceUrl: config.serviceUrl,
|
|
196
|
+
apiKeyFingerprint: config.apiKey ? config.apiKey.slice(0, 4) + config.apiKey.slice(-4) : '',
|
|
183
197
|
});
|
|
184
198
|
return crypto.createHash('sha256').update(payload).digest('hex');
|
|
185
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
|
|
|
@@ -162,23 +164,29 @@ export default {
|
|
|
162
164
|
version: PLUGIN_VERSION,
|
|
163
165
|
|
|
164
166
|
register(api: OpenClawPluginApi) {
|
|
167
|
+
_ocPluginConfig = api.pluginConfig ?? {};
|
|
168
|
+
log = api.logger ?? console;
|
|
169
|
+
|
|
165
170
|
if (!getConfig().enabled) {
|
|
166
|
-
|
|
171
|
+
log.info('[SafeClaw] Plugin disabled');
|
|
167
172
|
return;
|
|
168
173
|
}
|
|
169
174
|
|
|
170
|
-
log = api.logger ?? console;
|
|
171
|
-
_ocPluginConfig = api.pluginConfig ?? {};
|
|
172
|
-
|
|
173
175
|
// Generate a unique instance ID for this plugin run (fallback when agentId is not configured)
|
|
174
176
|
const instanceId = getConfig().agentId || `instance-${crypto.randomUUID()}`;
|
|
175
177
|
|
|
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/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.3.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.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/Status.tsx
CHANGED
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';
|
|
@@ -49,7 +49,10 @@ export function loadConfig(): SafeClawConfig {
|
|
|
49
49
|
if (raw.enabled === false) defaults.enabled = false;
|
|
50
50
|
if (raw.remote?.serviceUrl) defaults.serviceUrl = raw.remote.serviceUrl;
|
|
51
51
|
if (raw.remote?.apiKey) defaults.apiKey = raw.remote.apiKey;
|
|
52
|
-
if (raw.remote?.timeoutMs
|
|
52
|
+
if (raw.remote?.timeoutMs != null) {
|
|
53
|
+
const t = Number(raw.remote.timeoutMs);
|
|
54
|
+
if (Number.isFinite(t) && t > 0) defaults.timeoutMs = t;
|
|
55
|
+
}
|
|
53
56
|
if (raw.enforcement?.mode) defaults.enforcement = raw.enforcement.mode;
|
|
54
57
|
if (raw.enforcement?.failMode) defaults.failMode = raw.enforcement.failMode;
|
|
55
58
|
if (raw.agentId) defaults.agentId = raw.agentId;
|
|
@@ -73,6 +76,7 @@ export function loadConfig(): SafeClawConfig {
|
|
|
73
76
|
}
|
|
74
77
|
}
|
|
75
78
|
if (process.env.SAFECLAW_ENABLED === 'false') defaults.enabled = false;
|
|
79
|
+
else if (process.env.SAFECLAW_ENABLED === 'true') defaults.enabled = true;
|
|
76
80
|
const enforcement = process.env.SAFECLAW_ENFORCEMENT;
|
|
77
81
|
if (enforcement && ['enforce', 'warn-only', 'audit-only', 'disabled'].includes(enforcement)) {
|
|
78
82
|
defaults.enforcement = enforcement as SafeClawConfig['enforcement'];
|
|
@@ -152,6 +156,7 @@ export function saveConfig(config: SafeClawConfig): void {
|
|
|
152
156
|
existing.remote = {};
|
|
153
157
|
}
|
|
154
158
|
(existing.remote as Record<string, unknown>).serviceUrl = config.serviceUrl;
|
|
159
|
+
(existing.remote as Record<string, unknown>).timeoutMs = config.timeoutMs;
|
|
155
160
|
if (config.apiKey) {
|
|
156
161
|
(existing.remote as Record<string, unknown>).apiKey = config.apiKey;
|
|
157
162
|
} else {
|
|
@@ -176,10 +181,14 @@ export function saveConfig(config: SafeClawConfig): void {
|
|
|
176
181
|
|
|
177
182
|
try {
|
|
178
183
|
writeFileSync(CONFIG_PATH, JSON.stringify(existing, null, 2) + '\n', { encoding: 'utf-8', mode: 0o600 });
|
|
184
|
+
try { chmodSync(CONFIG_PATH, 0o600); } catch { /* best-effort */ }
|
|
179
185
|
} catch (e) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
186
|
+
const code = (e as NodeJS.ErrnoException).code;
|
|
187
|
+
if (code === 'EROFS') {
|
|
188
|
+
throw new Error('Cannot save config: filesystem is read-only (sandbox environment)');
|
|
189
|
+
}
|
|
190
|
+
if (code === 'EACCES') {
|
|
191
|
+
throw new Error(`Cannot save config: permission denied for ${CONFIG_PATH}`);
|
|
183
192
|
}
|
|
184
193
|
throw e;
|
|
185
194
|
}
|
|
@@ -195,6 +204,7 @@ export function configHash(config: SafeClawConfig): string {
|
|
|
195
204
|
enforcement: config.enforcement,
|
|
196
205
|
failMode: config.failMode,
|
|
197
206
|
serviceUrl: config.serviceUrl,
|
|
207
|
+
apiKeyFingerprint: config.apiKey ? config.apiKey.slice(0, 4) + config.apiKey.slice(-4) : '',
|
|
198
208
|
});
|
|
199
209
|
return crypto.createHash('sha256').update(payload).digest('hex');
|
|
200
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;
|