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 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 8 checks: config, API key, service health, evaluate endpoint,');
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 8 checks: config, API key, service health, evaluate endpoint,');
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
  }
@@ -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
- saveConfig(updated);
21
- onConfigChange(updated);
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
- try {
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
  }
@@ -67,10 +67,14 @@ export function loadConfig() {
67
67
  }
68
68
  if (process.env.SAFECLAW_ENABLED === 'false')
69
69
  defaults.enabled = false;
70
- if (process.env.SAFECLAW_ENFORCEMENT)
71
- defaults.enforcement = process.env.SAFECLAW_ENFORCEMENT;
72
- if (process.env.SAFECLAW_FAIL_MODE)
73
- defaults.failMode = process.env.SAFECLAW_FAIL_MODE;
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
  }
@@ -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.1.0",
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.2.0",
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
- saveConfig(updated);
36
- onConfigChange(updated);
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
- try {
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
- if (process.env.SAFECLAW_ENFORCEMENT) defaults.enforcement = process.env.SAFECLAW_ENFORCEMENT as SafeClawConfig['enforcement'];
77
- if (process.env.SAFECLAW_FAIL_MODE) defaults.failMode = process.env.SAFECLAW_FAIL_MODE as SafeClawConfig['failMode'];
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
+ }