openclaw-safeclaw-plugin 1.2.0 → 1.4.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/cli.tsx +31 -29
- package/dist/cli.js +30 -28
- package/dist/index.js +15 -5
- package/dist/tui/Settings.js +11 -10
- package/dist/tui/Status.js +1 -1
- package/dist/tui/config.js +25 -9
- package/index.ts +15 -6
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/tui/Settings.tsx +15 -8
- package/tui/Status.tsx +1 -1
- package/tui/config.ts +22 -6
- package/types/openclaw-sdk.d.ts +89 -0
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 8 checks: config, API key, service health, evaluate endpoint,');
|
|
494
|
-
console.log(' handshake, OpenClaw binary, plugin files, OpenClaw config');
|
|
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 8 checks: config, API key, service health, evaluate endpoint,');
|
|
505
|
-
console.log(' handshake, OpenClaw binary, plugin files, OpenClaw config');
|
|
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
|
@@ -34,7 +34,7 @@ function getConfig() {
|
|
|
34
34
|
// --- HTTP Client ---
|
|
35
35
|
async function post(path, body) {
|
|
36
36
|
const cfg = getConfig();
|
|
37
|
-
if (!cfg.enabled)
|
|
37
|
+
if (!cfg.enabled || cfg.enforcement === 'disabled')
|
|
38
38
|
return null;
|
|
39
39
|
const headers = { 'Content-Type': 'application/json' };
|
|
40
40
|
if (cfg.apiKey) {
|
|
@@ -79,7 +79,7 @@ async function post(path, body) {
|
|
|
79
79
|
}
|
|
80
80
|
async function get(path) {
|
|
81
81
|
const cfg = getConfig();
|
|
82
|
-
if (!cfg.enabled)
|
|
82
|
+
if (!cfg.enabled || cfg.enforcement === 'disabled')
|
|
83
83
|
return null;
|
|
84
84
|
const headers = {};
|
|
85
85
|
if (cfg.apiKey)
|
|
@@ -149,12 +149,12 @@ export default {
|
|
|
149
149
|
name: 'SafeClaw Neurosymbolic Governance',
|
|
150
150
|
version: PLUGIN_VERSION,
|
|
151
151
|
register(api) {
|
|
152
|
+
_ocPluginConfig = api.pluginConfig ?? {};
|
|
153
|
+
log = api.logger ?? console;
|
|
152
154
|
if (!getConfig().enabled) {
|
|
153
|
-
|
|
155
|
+
log.info('[SafeClaw] Plugin disabled');
|
|
154
156
|
return;
|
|
155
157
|
}
|
|
156
|
-
log = api.logger ?? console;
|
|
157
|
-
_ocPluginConfig = api.pluginConfig ?? {};
|
|
158
158
|
// Generate a unique instance ID for this plugin run (fallback when agentId is not configured)
|
|
159
159
|
const instanceId = getConfig().agentId || `instance-${crypto.randomUUID()}`;
|
|
160
160
|
// Heartbeat watchdog — send config hash to service every 30s
|
|
@@ -333,6 +333,16 @@ export default {
|
|
|
333
333
|
childConfig: event.childConfig ?? {},
|
|
334
334
|
reason: event.reason ?? '',
|
|
335
335
|
});
|
|
336
|
+
// Fail-closed handling (matches before_tool_call / message_sending pattern)
|
|
337
|
+
if (r === null && cfg.failMode === 'closed' && cfg.enforcement === 'enforce') {
|
|
338
|
+
throw new Error('SafeClaw service unavailable (fail-closed mode)');
|
|
339
|
+
}
|
|
340
|
+
else if (r === null && cfg.failMode === 'closed' && cfg.enforcement === 'warn-only') {
|
|
341
|
+
log.warn('[SafeClaw] Service unavailable (fail-closed mode, warn-only)');
|
|
342
|
+
}
|
|
343
|
+
else if (r === null && cfg.failMode === 'closed' && cfg.enforcement === 'audit-only') {
|
|
344
|
+
log.warn('[SafeClaw] Service unavailable (fail-closed mode, audit-only)');
|
|
345
|
+
}
|
|
336
346
|
if (r?.block && cfg.enforcement === 'enforce') {
|
|
337
347
|
throw new Error(r.reason || 'Blocked by SafeClaw: delegation bypass detected');
|
|
338
348
|
}
|
package/dist/tui/Settings.js
CHANGED
|
@@ -15,10 +15,17 @@ export default function Settings({ config, onConfigChange }) {
|
|
|
15
15
|
const [selected, setSelected] = useState(0);
|
|
16
16
|
const [editing, setEditing] = useState(false);
|
|
17
17
|
const [editBuffer, setEditBuffer] = useState('');
|
|
18
|
+
const [saveError, setSaveError] = useState(null);
|
|
18
19
|
const updateConfig = (patch) => {
|
|
19
20
|
const updated = { ...config, ...patch };
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
try {
|
|
22
|
+
saveConfig(updated);
|
|
23
|
+
setSaveError(null);
|
|
24
|
+
onConfigChange(updated);
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
setSaveError(e instanceof Error ? e.message : String(e));
|
|
28
|
+
}
|
|
22
29
|
};
|
|
23
30
|
useInput((input, key) => {
|
|
24
31
|
if (editing) {
|
|
@@ -28,13 +35,7 @@ export default function Settings({ config, onConfigChange }) {
|
|
|
28
35
|
// Reject invalid API keys — must start with sc_
|
|
29
36
|
return;
|
|
30
37
|
}
|
|
31
|
-
|
|
32
|
-
updateConfig({ [editingKey]: editBuffer });
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
// saveConfig may throw on invalid URL — stay in edit mode
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
+
updateConfig({ [editingKey]: editBuffer });
|
|
38
39
|
setEditing(false);
|
|
39
40
|
}
|
|
40
41
|
else if (key.escape) {
|
|
@@ -99,5 +100,5 @@ export default function Settings({ config, onConfigChange }) {
|
|
|
99
100
|
? SETTINGS[selected].key === 'apiKey' && editBuffer && !editBuffer.startsWith('sc_')
|
|
100
101
|
? ' API key must start with sc_ · esc to cancel'
|
|
101
102
|
: ' type to edit · enter to save · esc to cancel'
|
|
102
|
-
: ' ↑↓ navigate · ←→/enter cycle/toggle · enter to edit text fields · q quit' }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: ' Enforcement: enforce=block violations, warn-only=log only, audit-only=record only, disabled=off' }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: ' Fail Mode: open=allow if service unreachable, closed=block if service unreachable' }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: ' All changes save to ~/.safeclaw/config.json immediately.' }) })] }));
|
|
103
|
+
: ' ↑↓ navigate · ←→/enter cycle/toggle · enter to edit text fields · q quit' }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: ' Enforcement: enforce=block violations, warn-only=log only, audit-only=record only, disabled=off' }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: ' Fail Mode: open=allow if service unreachable, closed=block if service unreachable' }) }), saveError && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: [' Error saving config: ', saveError] }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: ' All changes save to ~/.safeclaw/config.json immediately.' }) })] }));
|
|
103
104
|
}
|
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
|
@@ -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,10 +70,16 @@ export function loadConfig() {
|
|
|
67
70
|
}
|
|
68
71
|
if (process.env.SAFECLAW_ENABLED === 'false')
|
|
69
72
|
defaults.enabled = false;
|
|
70
|
-
if (process.env.
|
|
71
|
-
defaults.
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
else if (process.env.SAFECLAW_ENABLED === 'true')
|
|
74
|
+
defaults.enabled = true;
|
|
75
|
+
const enforcement = process.env.SAFECLAW_ENFORCEMENT;
|
|
76
|
+
if (enforcement && ['enforce', 'warn-only', 'audit-only', 'disabled'].includes(enforcement)) {
|
|
77
|
+
defaults.enforcement = enforcement;
|
|
78
|
+
}
|
|
79
|
+
const failMode = process.env.SAFECLAW_FAIL_MODE;
|
|
80
|
+
if (failMode && ['open', 'closed'].includes(failMode)) {
|
|
81
|
+
defaults.failMode = failMode;
|
|
82
|
+
}
|
|
74
83
|
if (process.env.SAFECLAW_AGENT_ID)
|
|
75
84
|
defaults.agentId = process.env.SAFECLAW_AGENT_ID;
|
|
76
85
|
if (process.env.SAFECLAW_AGENT_TOKEN)
|
|
@@ -136,9 +145,13 @@ export function saveConfig(config) {
|
|
|
136
145
|
existing.remote = {};
|
|
137
146
|
}
|
|
138
147
|
existing.remote.serviceUrl = config.serviceUrl;
|
|
148
|
+
existing.remote.timeoutMs = config.timeoutMs;
|
|
139
149
|
if (config.apiKey) {
|
|
140
150
|
existing.remote.apiKey = config.apiKey;
|
|
141
151
|
}
|
|
152
|
+
else {
|
|
153
|
+
delete existing.remote.apiKey;
|
|
154
|
+
}
|
|
142
155
|
if (!existing.enforcement || typeof existing.enforcement !== 'object') {
|
|
143
156
|
existing.enforcement = {};
|
|
144
157
|
}
|
|
@@ -156,9 +169,12 @@ export function saveConfig(config) {
|
|
|
156
169
|
writeFileSync(CONFIG_PATH, JSON.stringify(existing, null, 2) + '\n', { encoding: 'utf-8', mode: 0o600 });
|
|
157
170
|
}
|
|
158
171
|
catch (e) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
172
|
+
const code = e.code;
|
|
173
|
+
if (code === 'EROFS') {
|
|
174
|
+
throw new Error('Cannot save config: filesystem is read-only (sandbox environment)');
|
|
175
|
+
}
|
|
176
|
+
if (code === 'EACCES') {
|
|
177
|
+
throw new Error(`Cannot save config: permission denied for ${CONFIG_PATH}`);
|
|
162
178
|
}
|
|
163
179
|
throw e;
|
|
164
180
|
}
|
package/index.ts
CHANGED
|
@@ -45,7 +45,7 @@ function getConfig(): typeof config {
|
|
|
45
45
|
|
|
46
46
|
async function post(path: string, body: Record<string, unknown>): Promise<Record<string, unknown> | null> {
|
|
47
47
|
const cfg = getConfig();
|
|
48
|
-
if (!cfg.enabled) return null;
|
|
48
|
+
if (!cfg.enabled || cfg.enforcement === 'disabled') return null;
|
|
49
49
|
|
|
50
50
|
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
51
51
|
if (cfg.apiKey) {
|
|
@@ -89,7 +89,7 @@ async function post(path: string, body: Record<string, unknown>): Promise<Record
|
|
|
89
89
|
|
|
90
90
|
async function get(path: string): Promise<Record<string, unknown> | null> {
|
|
91
91
|
const cfg = getConfig();
|
|
92
|
-
if (!cfg.enabled) return null;
|
|
92
|
+
if (!cfg.enabled || cfg.enforcement === 'disabled') return null;
|
|
93
93
|
const headers: Record<string, string> = {};
|
|
94
94
|
if (cfg.apiKey) headers['Authorization'] = `Bearer ${cfg.apiKey}`;
|
|
95
95
|
try {
|
|
@@ -162,14 +162,14 @@ export default {
|
|
|
162
162
|
version: PLUGIN_VERSION,
|
|
163
163
|
|
|
164
164
|
register(api: OpenClawPluginApi) {
|
|
165
|
+
_ocPluginConfig = api.pluginConfig ?? {};
|
|
166
|
+
log = api.logger ?? console;
|
|
167
|
+
|
|
165
168
|
if (!getConfig().enabled) {
|
|
166
|
-
|
|
169
|
+
log.info('[SafeClaw] Plugin disabled');
|
|
167
170
|
return;
|
|
168
171
|
}
|
|
169
172
|
|
|
170
|
-
log = api.logger ?? console;
|
|
171
|
-
_ocPluginConfig = api.pluginConfig ?? {};
|
|
172
|
-
|
|
173
173
|
// Generate a unique instance ID for this plugin run (fallback when agentId is not configured)
|
|
174
174
|
const instanceId = getConfig().agentId || `instance-${crypto.randomUUID()}`;
|
|
175
175
|
|
|
@@ -357,6 +357,15 @@ export default {
|
|
|
357
357
|
reason: event.reason ?? '',
|
|
358
358
|
});
|
|
359
359
|
|
|
360
|
+
// Fail-closed handling (matches before_tool_call / message_sending pattern)
|
|
361
|
+
if (r === null && cfg.failMode === 'closed' && cfg.enforcement === 'enforce') {
|
|
362
|
+
throw new Error('SafeClaw service unavailable (fail-closed mode)');
|
|
363
|
+
} else if (r === null && cfg.failMode === 'closed' && cfg.enforcement === 'warn-only') {
|
|
364
|
+
log.warn('[SafeClaw] Service unavailable (fail-closed mode, warn-only)');
|
|
365
|
+
} else if (r === null && cfg.failMode === 'closed' && cfg.enforcement === 'audit-only') {
|
|
366
|
+
log.warn('[SafeClaw] Service unavailable (fail-closed mode, audit-only)');
|
|
367
|
+
}
|
|
368
|
+
|
|
360
369
|
if (r?.block && cfg.enforcement === 'enforce') {
|
|
361
370
|
throw new Error((r.reason as string) || 'Blocked by SafeClaw: delegation bypass detected');
|
|
362
371
|
}
|
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.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",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
22
|
"dist/",
|
|
23
|
+
"types/",
|
|
23
24
|
"openclaw.plugin.json",
|
|
24
25
|
"index.ts",
|
|
25
26
|
"cli.tsx",
|
package/tui/Settings.tsx
CHANGED
|
@@ -29,11 +29,17 @@ export default function Settings({ config, onConfigChange }: SettingsProps) {
|
|
|
29
29
|
const [selected, setSelected] = useState(0);
|
|
30
30
|
const [editing, setEditing] = useState(false);
|
|
31
31
|
const [editBuffer, setEditBuffer] = useState('');
|
|
32
|
+
const [saveError, setSaveError] = useState<string | null>(null);
|
|
32
33
|
|
|
33
34
|
const updateConfig = (patch: Partial<SafeClawConfig>) => {
|
|
34
35
|
const updated = { ...config, ...patch };
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
try {
|
|
37
|
+
saveConfig(updated);
|
|
38
|
+
setSaveError(null);
|
|
39
|
+
onConfigChange(updated);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
setSaveError(e instanceof Error ? e.message : String(e));
|
|
42
|
+
}
|
|
37
43
|
};
|
|
38
44
|
|
|
39
45
|
useInput((input, key) => {
|
|
@@ -44,12 +50,7 @@ export default function Settings({ config, onConfigChange }: SettingsProps) {
|
|
|
44
50
|
// Reject invalid API keys — must start with sc_
|
|
45
51
|
return;
|
|
46
52
|
}
|
|
47
|
-
|
|
48
|
-
updateConfig({ [editingKey]: editBuffer } as Partial<SafeClawConfig>);
|
|
49
|
-
} catch {
|
|
50
|
-
// saveConfig may throw on invalid URL — stay in edit mode
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
+
updateConfig({ [editingKey]: editBuffer } as Partial<SafeClawConfig>);
|
|
53
54
|
setEditing(false);
|
|
54
55
|
} else if (key.escape) {
|
|
55
56
|
setEditing(false);
|
|
@@ -146,6 +147,12 @@ export default function Settings({ config, onConfigChange }: SettingsProps) {
|
|
|
146
147
|
<Text dimColor>{' Fail Mode: open=allow if service unreachable, closed=block if service unreachable'}</Text>
|
|
147
148
|
</Box>
|
|
148
149
|
|
|
150
|
+
{saveError && (
|
|
151
|
+
<Box marginTop={1}>
|
|
152
|
+
<Text color="red">{' Error saving config: '}{saveError}</Text>
|
|
153
|
+
</Box>
|
|
154
|
+
)}
|
|
155
|
+
|
|
149
156
|
<Box marginTop={1}>
|
|
150
157
|
<Text dimColor>{' All changes save to ~/.safeclaw/config.json immediately.'}</Text>
|
|
151
158
|
</Box>
|
package/tui/Status.tsx
CHANGED
package/tui/config.ts
CHANGED
|
@@ -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,8 +76,15 @@ export function loadConfig(): SafeClawConfig {
|
|
|
73
76
|
}
|
|
74
77
|
}
|
|
75
78
|
if (process.env.SAFECLAW_ENABLED === 'false') defaults.enabled = false;
|
|
76
|
-
if (process.env.
|
|
77
|
-
|
|
79
|
+
else if (process.env.SAFECLAW_ENABLED === 'true') defaults.enabled = true;
|
|
80
|
+
const enforcement = process.env.SAFECLAW_ENFORCEMENT;
|
|
81
|
+
if (enforcement && ['enforce', 'warn-only', 'audit-only', 'disabled'].includes(enforcement)) {
|
|
82
|
+
defaults.enforcement = enforcement as SafeClawConfig['enforcement'];
|
|
83
|
+
}
|
|
84
|
+
const failMode = process.env.SAFECLAW_FAIL_MODE;
|
|
85
|
+
if (failMode && ['open', 'closed'].includes(failMode)) {
|
|
86
|
+
defaults.failMode = failMode as SafeClawConfig['failMode'];
|
|
87
|
+
}
|
|
78
88
|
if (process.env.SAFECLAW_AGENT_ID) defaults.agentId = process.env.SAFECLAW_AGENT_ID;
|
|
79
89
|
if (process.env.SAFECLAW_AGENT_TOKEN) defaults.agentToken = process.env.SAFECLAW_AGENT_TOKEN;
|
|
80
90
|
|
|
@@ -146,8 +156,11 @@ export function saveConfig(config: SafeClawConfig): void {
|
|
|
146
156
|
existing.remote = {};
|
|
147
157
|
}
|
|
148
158
|
(existing.remote as Record<string, unknown>).serviceUrl = config.serviceUrl;
|
|
159
|
+
(existing.remote as Record<string, unknown>).timeoutMs = config.timeoutMs;
|
|
149
160
|
if (config.apiKey) {
|
|
150
161
|
(existing.remote as Record<string, unknown>).apiKey = config.apiKey;
|
|
162
|
+
} else {
|
|
163
|
+
delete (existing.remote as Record<string, unknown>).apiKey;
|
|
151
164
|
}
|
|
152
165
|
|
|
153
166
|
if (!existing.enforcement || typeof existing.enforcement !== 'object') {
|
|
@@ -169,9 +182,12 @@ export function saveConfig(config: SafeClawConfig): void {
|
|
|
169
182
|
try {
|
|
170
183
|
writeFileSync(CONFIG_PATH, JSON.stringify(existing, null, 2) + '\n', { encoding: 'utf-8', mode: 0o600 });
|
|
171
184
|
} catch (e) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
185
|
+
const code = (e as NodeJS.ErrnoException).code;
|
|
186
|
+
if (code === 'EROFS') {
|
|
187
|
+
throw new Error('Cannot save config: filesystem is read-only (sandbox environment)');
|
|
188
|
+
}
|
|
189
|
+
if (code === 'EACCES') {
|
|
190
|
+
throw new Error(`Cannot save config: permission denied for ${CONFIG_PATH}`);
|
|
175
191
|
}
|
|
176
192
|
throw e;
|
|
177
193
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ambient type declarations for OpenClaw Plugin SDK.
|
|
3
|
+
* These match the types exported by openclaw/plugin-sdk as of v2026.3.
|
|
4
|
+
* When installed inside OpenClaw, the real SDK types take precedence.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
declare module 'openclaw/plugin-sdk/core' {
|
|
8
|
+
export interface OpenClawPluginApi {
|
|
9
|
+
on(
|
|
10
|
+
hookName: string,
|
|
11
|
+
handler: (event: OpenClawPluginEvent, ctx: OpenClawPluginContext) => Promise<Record<string, unknown> | void> | void,
|
|
12
|
+
options?: { priority?: number },
|
|
13
|
+
): void;
|
|
14
|
+
registerService?(service: OpenClawPluginService): void;
|
|
15
|
+
registerCli?(registrar: (ctx: { program: unknown; config: unknown }) => void, opts?: { commands: string[] }): void;
|
|
16
|
+
registerTool?(tool: OpenClawPluginTool): void;
|
|
17
|
+
registerCommand?(def: { name: string; description: string; execute: (ctx: unknown) => Promise<string | void> }): void;
|
|
18
|
+
pluginConfig?: Record<string, unknown>;
|
|
19
|
+
logger?: OpenClawPluginLogger;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface OpenClawPluginEvent {
|
|
23
|
+
sessionId?: string;
|
|
24
|
+
toolName?: string;
|
|
25
|
+
params?: Record<string, unknown>;
|
|
26
|
+
to?: string;
|
|
27
|
+
content?: string;
|
|
28
|
+
result?: unknown;
|
|
29
|
+
error?: string;
|
|
30
|
+
durationMs?: number;
|
|
31
|
+
prompt?: string;
|
|
32
|
+
provider?: string;
|
|
33
|
+
model?: string;
|
|
34
|
+
lastAssistant?: string;
|
|
35
|
+
usage?: Record<string, unknown>;
|
|
36
|
+
parentAgentId?: string;
|
|
37
|
+
childConfig?: Record<string, unknown>;
|
|
38
|
+
reason?: string;
|
|
39
|
+
metadata?: Record<string, unknown>;
|
|
40
|
+
channel?: string;
|
|
41
|
+
sender?: string;
|
|
42
|
+
[key: string]: unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface OpenClawPluginContext {
|
|
46
|
+
sessionId?: string;
|
|
47
|
+
agentId?: string;
|
|
48
|
+
runId?: string;
|
|
49
|
+
conversationId?: string;
|
|
50
|
+
accountId?: string;
|
|
51
|
+
channelId?: string;
|
|
52
|
+
workspaceDir?: string;
|
|
53
|
+
[key: string]: unknown;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface OpenClawPluginService {
|
|
57
|
+
id: string;
|
|
58
|
+
start: () => void | Promise<void>;
|
|
59
|
+
stop?: () => void | Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface OpenClawPluginTool {
|
|
63
|
+
name: string;
|
|
64
|
+
description: string;
|
|
65
|
+
parameters: Record<string, unknown>;
|
|
66
|
+
execute: (params: Record<string, unknown>, ctx: Record<string, unknown>) => Promise<unknown>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface OpenClawPluginLogger {
|
|
70
|
+
info: (...args: unknown[]) => void;
|
|
71
|
+
warn: (...args: unknown[]) => void;
|
|
72
|
+
error: (...args: unknown[]) => void;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
declare module 'openclaw/plugin-sdk/plugin-entry' {
|
|
77
|
+
import type { OpenClawPluginApi } from 'openclaw/plugin-sdk/core';
|
|
78
|
+
|
|
79
|
+
export interface PluginDefinition {
|
|
80
|
+
id: string;
|
|
81
|
+
name: string;
|
|
82
|
+
description?: string;
|
|
83
|
+
version?: string;
|
|
84
|
+
configSchema?: Record<string, unknown>;
|
|
85
|
+
register?: (api: OpenClawPluginApi) => void | Promise<void>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function definePluginEntry(def: PluginDefinition): PluginDefinition;
|
|
89
|
+
}
|