openclaw-safeclaw-plugin 1.2.0 → 1.3.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 +2 -2
- package/dist/cli.js +2 -2
- package/dist/index.js +12 -2
- package/dist/tui/Settings.js +11 -10
- package/dist/tui/config.js +11 -4
- package/index.ts +11 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/tui/Settings.tsx +15 -8
- package/tui/config.ts +10 -2
- package/types/openclaw-sdk.d.ts +89 -0
package/cli.tsx
CHANGED
|
@@ -490,8 +490,8 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
|
|
|
490
490
|
console.log(' restart-openclaw Restart the OpenClaw daemon to pick up plugin changes');
|
|
491
491
|
console.log('');
|
|
492
492
|
console.log('Diagnostics:');
|
|
493
|
-
console.log(' status Run
|
|
494
|
-
console.log(' handshake, OpenClaw binary, plugin files, OpenClaw config');
|
|
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
495
|
console.log('');
|
|
496
496
|
console.log('Configuration:');
|
|
497
497
|
console.log(' config show Show current enforcement, failMode, enabled, serviceUrl, apiKey');
|
package/dist/cli.js
CHANGED
|
@@ -501,8 +501,8 @@ else {
|
|
|
501
501
|
console.log(' restart-openclaw Restart the OpenClaw daemon to pick up plugin changes');
|
|
502
502
|
console.log('');
|
|
503
503
|
console.log('Diagnostics:');
|
|
504
|
-
console.log(' status Run
|
|
505
|
-
console.log(' handshake, OpenClaw binary, plugin files, OpenClaw config');
|
|
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
506
|
console.log('');
|
|
507
507
|
console.log('Configuration:');
|
|
508
508
|
console.log(' config show Show current enforcement, failMode, enabled, serviceUrl, apiKey');
|
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)
|
|
@@ -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/config.js
CHANGED
|
@@ -67,10 +67,14 @@ export function loadConfig() {
|
|
|
67
67
|
}
|
|
68
68
|
if (process.env.SAFECLAW_ENABLED === 'false')
|
|
69
69
|
defaults.enabled = false;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
const enforcement = process.env.SAFECLAW_ENFORCEMENT;
|
|
71
|
+
if (enforcement && ['enforce', 'warn-only', 'audit-only', 'disabled'].includes(enforcement)) {
|
|
72
|
+
defaults.enforcement = enforcement;
|
|
73
|
+
}
|
|
74
|
+
const failMode = process.env.SAFECLAW_FAIL_MODE;
|
|
75
|
+
if (failMode && ['open', 'closed'].includes(failMode)) {
|
|
76
|
+
defaults.failMode = failMode;
|
|
77
|
+
}
|
|
74
78
|
if (process.env.SAFECLAW_AGENT_ID)
|
|
75
79
|
defaults.agentId = process.env.SAFECLAW_AGENT_ID;
|
|
76
80
|
if (process.env.SAFECLAW_AGENT_TOKEN)
|
|
@@ -139,6 +143,9 @@ export function saveConfig(config) {
|
|
|
139
143
|
if (config.apiKey) {
|
|
140
144
|
existing.remote.apiKey = config.apiKey;
|
|
141
145
|
}
|
|
146
|
+
else {
|
|
147
|
+
delete existing.remote.apiKey;
|
|
148
|
+
}
|
|
142
149
|
if (!existing.enforcement || typeof existing.enforcement !== 'object') {
|
|
143
150
|
existing.enforcement = {};
|
|
144
151
|
}
|
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 {
|
|
@@ -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.2.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.3.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/config.ts
CHANGED
|
@@ -73,8 +73,14 @@ export function loadConfig(): SafeClawConfig {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
if (process.env.SAFECLAW_ENABLED === 'false') defaults.enabled = false;
|
|
76
|
-
|
|
77
|
-
if (
|
|
76
|
+
const enforcement = process.env.SAFECLAW_ENFORCEMENT;
|
|
77
|
+
if (enforcement && ['enforce', 'warn-only', 'audit-only', 'disabled'].includes(enforcement)) {
|
|
78
|
+
defaults.enforcement = enforcement as SafeClawConfig['enforcement'];
|
|
79
|
+
}
|
|
80
|
+
const failMode = process.env.SAFECLAW_FAIL_MODE;
|
|
81
|
+
if (failMode && ['open', 'closed'].includes(failMode)) {
|
|
82
|
+
defaults.failMode = failMode as SafeClawConfig['failMode'];
|
|
83
|
+
}
|
|
78
84
|
if (process.env.SAFECLAW_AGENT_ID) defaults.agentId = process.env.SAFECLAW_AGENT_ID;
|
|
79
85
|
if (process.env.SAFECLAW_AGENT_TOKEN) defaults.agentToken = process.env.SAFECLAW_AGENT_TOKEN;
|
|
80
86
|
|
|
@@ -148,6 +154,8 @@ export function saveConfig(config: SafeClawConfig): void {
|
|
|
148
154
|
(existing.remote as Record<string, unknown>).serviceUrl = config.serviceUrl;
|
|
149
155
|
if (config.apiKey) {
|
|
150
156
|
(existing.remote as Record<string, unknown>).apiKey = config.apiKey;
|
|
157
|
+
} else {
|
|
158
|
+
delete (existing.remote as Record<string, unknown>).apiKey;
|
|
151
159
|
}
|
|
152
160
|
|
|
153
161
|
if (!existing.enforcement || typeof existing.enforcement !== 'object') {
|
|
@@ -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
|
+
}
|