openclaw-safeclaw-plugin 0.1.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/README.md +77 -0
- package/SKILL.md +59 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +178 -0
- package/index.ts +222 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# openclaw-safeclaw-plugin
|
|
2
|
+
|
|
3
|
+
Neurosymbolic governance plugin for OpenClaw AI agents. Validates every tool call, message, and action against OWL ontologies and SHACL constraints before execution.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install openclaw-safeclaw-plugin
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start (SaaS)
|
|
12
|
+
|
|
13
|
+
Point the plugin at the hosted SafeClaw service:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
export SAFECLAW_URL="https://api.safeclaw.eu/api/v1"
|
|
17
|
+
export SAFECLAW_ENFORCEMENT="enforce"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
No server setup needed — the plugin connects to SafeClaw's hosted service.
|
|
21
|
+
|
|
22
|
+
## Quick Start (Self-Hosted)
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Start the SafeClaw service
|
|
26
|
+
git clone https://github.com/tendlyeu/SafeClaw.git
|
|
27
|
+
cd SafeClaw/safeclaw-service
|
|
28
|
+
python -m venv .venv && source .venv/bin/activate
|
|
29
|
+
pip install -e ".[dev]"
|
|
30
|
+
safeclaw init --user-id yourname
|
|
31
|
+
safeclaw serve
|
|
32
|
+
# Engine ready on http://localhost:8420
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The plugin auto-connects to `http://localhost:8420/api/v1` by default.
|
|
36
|
+
|
|
37
|
+
## What It Does
|
|
38
|
+
|
|
39
|
+
- **Blocks dangerous actions** — force push, deleting root, exposing secrets
|
|
40
|
+
- **Enforces dependencies** — tests must pass before git push
|
|
41
|
+
- **Checks user preferences** — confirmation for irreversible actions
|
|
42
|
+
- **Governs messages** — blocks sensitive data leaks
|
|
43
|
+
- **Full audit trail** — every decision logged with ontological justification
|
|
44
|
+
|
|
45
|
+
## How It Works
|
|
46
|
+
|
|
47
|
+
The plugin registers hooks on OpenClaw events:
|
|
48
|
+
|
|
49
|
+
1. **before_tool_call** — validates against SHACL shapes, policies, preferences, dependencies
|
|
50
|
+
2. **before_agent_start** — injects governance context into the agent's system prompt
|
|
51
|
+
3. **message_sending** — checks outbound messages for sensitive data
|
|
52
|
+
4. **after_tool_call** — records action outcomes for dependency tracking
|
|
53
|
+
5. **llm_input/output** — logs LLM interactions for audit
|
|
54
|
+
|
|
55
|
+
## Configuration
|
|
56
|
+
|
|
57
|
+
Set via environment variables or `~/.safeclaw/config.json`:
|
|
58
|
+
|
|
59
|
+
| Variable | Default | Description |
|
|
60
|
+
|----------|---------|-------------|
|
|
61
|
+
| `SAFECLAW_URL` | `http://localhost:8420/api/v1` | SafeClaw service URL |
|
|
62
|
+
| `SAFECLAW_API_KEY` | *(empty)* | API key for cloud mode |
|
|
63
|
+
| `SAFECLAW_TIMEOUT_MS` | `500` | Request timeout in ms |
|
|
64
|
+
| `SAFECLAW_ENABLED` | `true` | Set `false` to disable |
|
|
65
|
+
| `SAFECLAW_ENFORCEMENT` | `enforce` | `enforce`, `warn-only`, `audit-only`, or `disabled` |
|
|
66
|
+
| `SAFECLAW_FAIL_MODE` | `closed` | `open` (allow on failure) or `closed` (block on failure) |
|
|
67
|
+
|
|
68
|
+
## Enforcement Modes
|
|
69
|
+
|
|
70
|
+
- **`enforce`** — block actions that violate constraints (recommended)
|
|
71
|
+
- **`warn-only`** — log warnings but allow all actions
|
|
72
|
+
- **`audit-only`** — server-side logging only, no client-side action
|
|
73
|
+
- **`disabled`** — plugin is completely inactive
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# SafeClaw — Neurosymbolic Governance for OpenClaw
|
|
2
|
+
|
|
3
|
+
SafeClaw adds ontology-based constraint checking to your OpenClaw agent. Every tool call, message, and action is validated against OWL ontologies and SHACL shapes before execution.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
- **Blocks dangerous actions** — force push, deleting root, exposing secrets
|
|
8
|
+
- **Enforces dependencies** — tests must pass before git push
|
|
9
|
+
- **Checks user preferences** — confirmation for irreversible actions based on autonomy level
|
|
10
|
+
- **Governs messages** — blocks sensitive data leaks, enforces never-contact lists
|
|
11
|
+
- **Full audit trail** — every decision logged with ontological justification
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
### Option A: Local mode (self-hosted)
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# 1. Start the SafeClaw service
|
|
19
|
+
pip install safeclaw
|
|
20
|
+
safeclaw init
|
|
21
|
+
uvicorn safeclaw.main:app --port 8420
|
|
22
|
+
|
|
23
|
+
# 2. Install this plugin (done if you're reading this via clawhub)
|
|
24
|
+
# The plugin auto-connects to http://localhost:8420
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Option B: Cloud mode (safeclaw.eu)
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# 1. Sign up at safeclaw.eu and get your API key
|
|
31
|
+
|
|
32
|
+
# 2. Set your API key
|
|
33
|
+
export SAFECLAW_URL="https://api.safeclaw.eu/api/v1"
|
|
34
|
+
export SAFECLAW_API_KEY="sc_live_your_key_here"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Configuration
|
|
38
|
+
|
|
39
|
+
Set via environment variables or `~/.safeclaw/config.json`:
|
|
40
|
+
|
|
41
|
+
| Variable | Default | Description |
|
|
42
|
+
|----------|---------|-------------|
|
|
43
|
+
| `SAFECLAW_URL` | `http://localhost:8420/api/v1` | SafeClaw service URL |
|
|
44
|
+
| `SAFECLAW_API_KEY` | (empty) | API key for remote/cloud mode |
|
|
45
|
+
| `SAFECLAW_TIMEOUT_MS` | `500` | Request timeout in milliseconds |
|
|
46
|
+
| `SAFECLAW_ENABLED` | `true` | Set to `false` to disable |
|
|
47
|
+
| `SAFECLAW_ENFORCEMENT` | `enforce` | `enforce`, `warn-only`, `audit-only`, or `disabled` |
|
|
48
|
+
|
|
49
|
+
## How it works
|
|
50
|
+
|
|
51
|
+
This plugin registers hooks on every OpenClaw event:
|
|
52
|
+
|
|
53
|
+
1. **before_tool_call** — validates against SHACL shapes, policies, preferences, dependencies
|
|
54
|
+
2. **before_agent_start** — injects governance context into the agent's system prompt
|
|
55
|
+
3. **message_sending** — checks outbound messages for sensitive data and contact rules
|
|
56
|
+
4. **after_tool_call** — records action outcomes for dependency tracking
|
|
57
|
+
5. **llm_input/output** — logs LLM interactions for audit
|
|
58
|
+
|
|
59
|
+
If the SafeClaw service is unavailable, the plugin degrades gracefully — no blocks, no crashes.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SafeClaw — Neurosymbolic Governance Plugin for OpenClaw
|
|
3
|
+
*
|
|
4
|
+
* This TypeScript file is the ENTIRE client-side codebase.
|
|
5
|
+
* All governance logic lives in the SafeClaw Python service.
|
|
6
|
+
* This plugin is a thin HTTP bridge that forwards OpenClaw events
|
|
7
|
+
* to the SafeClaw service and acts on the responses.
|
|
8
|
+
*/
|
|
9
|
+
interface PluginEvent {
|
|
10
|
+
sessionId?: string;
|
|
11
|
+
userId?: string;
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
interface PluginContext {
|
|
15
|
+
sessionId?: string;
|
|
16
|
+
userId?: string;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
interface PluginApi {
|
|
20
|
+
on(event: string, handler: (event: PluginEvent, ctx: PluginContext) => Promise<Record<string, unknown> | void> | void, options?: {
|
|
21
|
+
priority?: number;
|
|
22
|
+
}): void;
|
|
23
|
+
}
|
|
24
|
+
declare const _default: {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
version: string;
|
|
28
|
+
register(api: PluginApi): void;
|
|
29
|
+
};
|
|
30
|
+
export default _default;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SafeClaw — Neurosymbolic Governance Plugin for OpenClaw
|
|
3
|
+
*
|
|
4
|
+
* This TypeScript file is the ENTIRE client-side codebase.
|
|
5
|
+
* All governance logic lives in the SafeClaw Python service.
|
|
6
|
+
* This plugin is a thin HTTP bridge that forwards OpenClaw events
|
|
7
|
+
* to the SafeClaw service and acts on the responses.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync, existsSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { homedir } from 'os';
|
|
12
|
+
function loadConfig() {
|
|
13
|
+
const defaults = {
|
|
14
|
+
serviceUrl: process.env.SAFECLAW_URL ?? 'http://localhost:8420/api/v1',
|
|
15
|
+
apiKey: process.env.SAFECLAW_API_KEY ?? '',
|
|
16
|
+
timeoutMs: parseInt(process.env.SAFECLAW_TIMEOUT_MS ?? '500', 10),
|
|
17
|
+
enabled: process.env.SAFECLAW_ENABLED !== 'false',
|
|
18
|
+
enforcement: process.env.SAFECLAW_ENFORCEMENT ?? 'enforce',
|
|
19
|
+
failMode: process.env.SAFECLAW_FAIL_MODE ?? 'closed',
|
|
20
|
+
agentId: process.env.SAFECLAW_AGENT_ID ?? '',
|
|
21
|
+
agentToken: process.env.SAFECLAW_AGENT_TOKEN ?? '',
|
|
22
|
+
};
|
|
23
|
+
// Try loading from config file
|
|
24
|
+
const configPath = join(homedir(), '.safeclaw', 'config.json');
|
|
25
|
+
if (existsSync(configPath)) {
|
|
26
|
+
try {
|
|
27
|
+
const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
28
|
+
if (raw.enabled === false)
|
|
29
|
+
defaults.enabled = false;
|
|
30
|
+
if (raw.remote?.serviceUrl)
|
|
31
|
+
defaults.serviceUrl = raw.remote.serviceUrl;
|
|
32
|
+
if (raw.remote?.apiKey)
|
|
33
|
+
defaults.apiKey = raw.remote.apiKey;
|
|
34
|
+
if (raw.remote?.timeoutMs)
|
|
35
|
+
defaults.timeoutMs = raw.remote.timeoutMs;
|
|
36
|
+
if (raw.enforcement?.mode)
|
|
37
|
+
defaults.enforcement = raw.enforcement.mode;
|
|
38
|
+
if (raw.enforcement?.failMode)
|
|
39
|
+
defaults.failMode = raw.enforcement.failMode;
|
|
40
|
+
if (raw.agentId)
|
|
41
|
+
defaults.agentId = raw.agentId;
|
|
42
|
+
if (raw.agentToken)
|
|
43
|
+
defaults.agentToken = raw.agentToken;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Config file unreadable — use defaults
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
defaults.serviceUrl = defaults.serviceUrl.replace(/\/+$/, '');
|
|
50
|
+
const validModes = ['enforce', 'warn-only', 'audit-only', 'disabled'];
|
|
51
|
+
if (!validModes.includes(defaults.enforcement)) {
|
|
52
|
+
console.warn(`[SafeClaw] Invalid enforcement mode "${defaults.enforcement}", defaulting to "enforce"`);
|
|
53
|
+
defaults.enforcement = 'enforce';
|
|
54
|
+
}
|
|
55
|
+
const validFailModes = ['open', 'closed'];
|
|
56
|
+
if (!validFailModes.includes(defaults.failMode)) {
|
|
57
|
+
console.warn(`[SafeClaw] Invalid fail mode "${defaults.failMode}", defaulting to "closed"`);
|
|
58
|
+
defaults.failMode = 'closed';
|
|
59
|
+
}
|
|
60
|
+
return defaults;
|
|
61
|
+
}
|
|
62
|
+
const config = loadConfig();
|
|
63
|
+
// --- HTTP Client ---
|
|
64
|
+
async function post(path, body) {
|
|
65
|
+
if (!config.enabled)
|
|
66
|
+
return null;
|
|
67
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
68
|
+
if (config.apiKey) {
|
|
69
|
+
headers['Authorization'] = `Bearer ${config.apiKey}`;
|
|
70
|
+
}
|
|
71
|
+
const agentFields = config.agentId ? { agentId: config.agentId, agentToken: config.agentToken } : {};
|
|
72
|
+
try {
|
|
73
|
+
const res = await fetch(`${config.serviceUrl}${path}`, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers,
|
|
76
|
+
body: JSON.stringify({ ...body, ...agentFields }),
|
|
77
|
+
signal: AbortSignal.timeout(config.timeoutMs),
|
|
78
|
+
});
|
|
79
|
+
if (!res.ok) {
|
|
80
|
+
console.warn(`[SafeClaw] HTTP ${res.status} from ${path}`);
|
|
81
|
+
return null; // Caller checks failMode
|
|
82
|
+
}
|
|
83
|
+
return await res.json();
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
if (e instanceof DOMException && e.name === 'TimeoutError') {
|
|
87
|
+
console.debug(`[SafeClaw] Timeout on ${path}`);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
console.debug(`[SafeClaw] Service unavailable: ${path}`);
|
|
91
|
+
}
|
|
92
|
+
return null; // Caller checks failMode
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export default {
|
|
96
|
+
id: 'openclaw-safeclaw-plugin',
|
|
97
|
+
name: 'SafeClaw Neurosymbolic Governance',
|
|
98
|
+
version: '0.1.0',
|
|
99
|
+
register(api) {
|
|
100
|
+
if (!config.enabled)
|
|
101
|
+
return;
|
|
102
|
+
// THE GATE — constraint checking on every tool call
|
|
103
|
+
api.on('before_tool_call', async (event, ctx) => {
|
|
104
|
+
const r = await post('/evaluate/tool-call', {
|
|
105
|
+
sessionId: ctx.sessionId ?? event.sessionId,
|
|
106
|
+
userId: ctx.userId ?? event.userId,
|
|
107
|
+
toolName: event.toolName ?? event.tool_name,
|
|
108
|
+
params: event.params ?? {},
|
|
109
|
+
sessionHistory: event.sessionHistory ?? [],
|
|
110
|
+
});
|
|
111
|
+
if (r === null && config.failMode === 'closed' && config.enforcement === 'enforce') {
|
|
112
|
+
return { block: true, blockReason: 'SafeClaw service unavailable (fail-closed)' };
|
|
113
|
+
}
|
|
114
|
+
else if (r === null && config.failMode === 'closed' && config.enforcement === 'warn-only') {
|
|
115
|
+
console.warn('[SafeClaw] Service unavailable (fail-closed mode, warn-only)');
|
|
116
|
+
}
|
|
117
|
+
if (r?.block) {
|
|
118
|
+
if (config.enforcement === 'enforce') {
|
|
119
|
+
return { block: true, blockReason: r.reason };
|
|
120
|
+
}
|
|
121
|
+
if (config.enforcement === 'warn-only') {
|
|
122
|
+
console.warn(`[SafeClaw] Warning: ${r.reason}`);
|
|
123
|
+
}
|
|
124
|
+
// audit-only: logged server-side, no action here
|
|
125
|
+
}
|
|
126
|
+
}, { priority: 100 });
|
|
127
|
+
// Context injection — prepend governance context to agent system prompt
|
|
128
|
+
api.on('before_agent_start', async (event, ctx) => {
|
|
129
|
+
const r = await post('/context/build', {
|
|
130
|
+
sessionId: ctx.sessionId ?? event.sessionId,
|
|
131
|
+
userId: ctx.userId ?? event.userId,
|
|
132
|
+
});
|
|
133
|
+
if (r?.prependContext) {
|
|
134
|
+
return { prependContext: r.prependContext };
|
|
135
|
+
}
|
|
136
|
+
}, { priority: 100 });
|
|
137
|
+
// Message governance — check outbound messages
|
|
138
|
+
api.on('message_sending', async (event, ctx) => {
|
|
139
|
+
const r = await post('/evaluate/message', {
|
|
140
|
+
sessionId: ctx.sessionId ?? event.sessionId,
|
|
141
|
+
userId: ctx.userId ?? event.userId,
|
|
142
|
+
to: event.to,
|
|
143
|
+
content: event.content,
|
|
144
|
+
});
|
|
145
|
+
if (r === null && config.failMode === 'closed' && config.enforcement === 'enforce') {
|
|
146
|
+
return { cancel: true };
|
|
147
|
+
}
|
|
148
|
+
else if (r === null && config.failMode === 'closed' && config.enforcement === 'warn-only') {
|
|
149
|
+
console.warn('[SafeClaw] Service unavailable (fail-closed mode, warn-only)');
|
|
150
|
+
}
|
|
151
|
+
if (r?.block && config.enforcement === 'enforce') {
|
|
152
|
+
return { cancel: true };
|
|
153
|
+
}
|
|
154
|
+
}, { priority: 100 });
|
|
155
|
+
// Async logging — fire-and-forget, no return value needed
|
|
156
|
+
api.on('llm_input', (event, ctx) => {
|
|
157
|
+
post('/log/llm-input', {
|
|
158
|
+
sessionId: ctx.sessionId ?? event.sessionId,
|
|
159
|
+
content: event.content,
|
|
160
|
+
}).catch(() => { });
|
|
161
|
+
});
|
|
162
|
+
api.on('llm_output', (event, ctx) => {
|
|
163
|
+
post('/log/llm-output', {
|
|
164
|
+
sessionId: ctx.sessionId ?? event.sessionId,
|
|
165
|
+
content: event.content,
|
|
166
|
+
}).catch(() => { });
|
|
167
|
+
});
|
|
168
|
+
api.on('after_tool_call', (event, ctx) => {
|
|
169
|
+
post('/record/tool-result', {
|
|
170
|
+
sessionId: ctx.sessionId ?? event.sessionId,
|
|
171
|
+
toolName: event.toolName ?? event.tool_name,
|
|
172
|
+
params: event.params ?? {},
|
|
173
|
+
result: event.result ?? '',
|
|
174
|
+
success: event.success ?? true,
|
|
175
|
+
}).catch(() => { });
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
};
|
package/index.ts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SafeClaw — Neurosymbolic Governance Plugin for OpenClaw
|
|
3
|
+
*
|
|
4
|
+
* This TypeScript file is the ENTIRE client-side codebase.
|
|
5
|
+
* All governance logic lives in the SafeClaw Python service.
|
|
6
|
+
* This plugin is a thin HTTP bridge that forwards OpenClaw events
|
|
7
|
+
* to the SafeClaw service and acts on the responses.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync, existsSync } from 'fs';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
import { homedir } from 'os';
|
|
13
|
+
|
|
14
|
+
// --- Configuration ---
|
|
15
|
+
|
|
16
|
+
interface SafeClawPluginConfig {
|
|
17
|
+
serviceUrl: string;
|
|
18
|
+
apiKey: string;
|
|
19
|
+
timeoutMs: number;
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
enforcement: 'enforce' | 'warn-only' | 'audit-only' | 'disabled';
|
|
22
|
+
failMode: 'open' | 'closed';
|
|
23
|
+
agentId: string;
|
|
24
|
+
agentToken: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function loadConfig(): SafeClawPluginConfig {
|
|
28
|
+
const defaults: SafeClawPluginConfig = {
|
|
29
|
+
serviceUrl: process.env.SAFECLAW_URL ?? 'http://localhost:8420/api/v1',
|
|
30
|
+
apiKey: process.env.SAFECLAW_API_KEY ?? '',
|
|
31
|
+
timeoutMs: parseInt(process.env.SAFECLAW_TIMEOUT_MS ?? '500', 10),
|
|
32
|
+
enabled: process.env.SAFECLAW_ENABLED !== 'false',
|
|
33
|
+
enforcement: (process.env.SAFECLAW_ENFORCEMENT as SafeClawPluginConfig['enforcement']) ?? 'enforce',
|
|
34
|
+
failMode: (process.env.SAFECLAW_FAIL_MODE as SafeClawPluginConfig['failMode']) ?? 'closed',
|
|
35
|
+
agentId: process.env.SAFECLAW_AGENT_ID ?? '',
|
|
36
|
+
agentToken: process.env.SAFECLAW_AGENT_TOKEN ?? '',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Try loading from config file
|
|
40
|
+
const configPath = join(homedir(), '.safeclaw', 'config.json');
|
|
41
|
+
if (existsSync(configPath)) {
|
|
42
|
+
try {
|
|
43
|
+
const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
44
|
+
if (raw.enabled === false) defaults.enabled = false;
|
|
45
|
+
if (raw.remote?.serviceUrl) defaults.serviceUrl = raw.remote.serviceUrl;
|
|
46
|
+
if (raw.remote?.apiKey) defaults.apiKey = raw.remote.apiKey;
|
|
47
|
+
if (raw.remote?.timeoutMs) defaults.timeoutMs = raw.remote.timeoutMs;
|
|
48
|
+
if (raw.enforcement?.mode) defaults.enforcement = raw.enforcement.mode;
|
|
49
|
+
if (raw.enforcement?.failMode) defaults.failMode = raw.enforcement.failMode;
|
|
50
|
+
if (raw.agentId) defaults.agentId = raw.agentId;
|
|
51
|
+
if (raw.agentToken) defaults.agentToken = raw.agentToken;
|
|
52
|
+
} catch {
|
|
53
|
+
// Config file unreadable — use defaults
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
defaults.serviceUrl = defaults.serviceUrl.replace(/\/+$/, '');
|
|
58
|
+
|
|
59
|
+
const validModes = ['enforce', 'warn-only', 'audit-only', 'disabled'] as const;
|
|
60
|
+
if (!validModes.includes(defaults.enforcement as any)) {
|
|
61
|
+
console.warn(`[SafeClaw] Invalid enforcement mode "${defaults.enforcement}", defaulting to "enforce"`);
|
|
62
|
+
defaults.enforcement = 'enforce';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const validFailModes = ['open', 'closed'] as const;
|
|
66
|
+
if (!validFailModes.includes(defaults.failMode as any)) {
|
|
67
|
+
console.warn(`[SafeClaw] Invalid fail mode "${defaults.failMode}", defaulting to "closed"`);
|
|
68
|
+
defaults.failMode = 'closed';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return defaults;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const config = loadConfig();
|
|
75
|
+
|
|
76
|
+
// --- HTTP Client ---
|
|
77
|
+
|
|
78
|
+
async function post(path: string, body: Record<string, unknown>): Promise<Record<string, unknown> | null> {
|
|
79
|
+
if (!config.enabled) return null;
|
|
80
|
+
|
|
81
|
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
82
|
+
if (config.apiKey) {
|
|
83
|
+
headers['Authorization'] = `Bearer ${config.apiKey}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const agentFields = config.agentId ? { agentId: config.agentId, agentToken: config.agentToken } : {};
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const res = await fetch(`${config.serviceUrl}${path}`, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers,
|
|
92
|
+
body: JSON.stringify({ ...body, ...agentFields }),
|
|
93
|
+
signal: AbortSignal.timeout(config.timeoutMs),
|
|
94
|
+
});
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
console.warn(`[SafeClaw] HTTP ${res.status} from ${path}`);
|
|
97
|
+
return null; // Caller checks failMode
|
|
98
|
+
}
|
|
99
|
+
return await res.json() as Record<string, unknown>;
|
|
100
|
+
} catch (e) {
|
|
101
|
+
if (e instanceof DOMException && e.name === 'TimeoutError') {
|
|
102
|
+
console.debug(`[SafeClaw] Timeout on ${path}`);
|
|
103
|
+
} else {
|
|
104
|
+
console.debug(`[SafeClaw] Service unavailable: ${path}`);
|
|
105
|
+
}
|
|
106
|
+
return null; // Caller checks failMode
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// --- Plugin Definition ---
|
|
111
|
+
|
|
112
|
+
interface PluginEvent {
|
|
113
|
+
sessionId?: string;
|
|
114
|
+
userId?: string;
|
|
115
|
+
[key: string]: unknown;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface PluginContext {
|
|
119
|
+
sessionId?: string;
|
|
120
|
+
userId?: string;
|
|
121
|
+
[key: string]: unknown;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface PluginApi {
|
|
125
|
+
on(
|
|
126
|
+
event: string,
|
|
127
|
+
handler: (event: PluginEvent, ctx: PluginContext) => Promise<Record<string, unknown> | void> | void,
|
|
128
|
+
options?: { priority?: number },
|
|
129
|
+
): void;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export default {
|
|
133
|
+
id: 'openclaw-safeclaw-plugin',
|
|
134
|
+
name: 'SafeClaw Neurosymbolic Governance',
|
|
135
|
+
version: '0.1.0',
|
|
136
|
+
|
|
137
|
+
register(api: PluginApi) {
|
|
138
|
+
if (!config.enabled) return;
|
|
139
|
+
|
|
140
|
+
// THE GATE — constraint checking on every tool call
|
|
141
|
+
api.on('before_tool_call', async (event: PluginEvent, ctx: PluginContext) => {
|
|
142
|
+
const r = await post('/evaluate/tool-call', {
|
|
143
|
+
sessionId: ctx.sessionId ?? event.sessionId,
|
|
144
|
+
userId: ctx.userId ?? event.userId,
|
|
145
|
+
toolName: event.toolName ?? event.tool_name,
|
|
146
|
+
params: event.params ?? {},
|
|
147
|
+
sessionHistory: event.sessionHistory ?? [],
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (r === null && config.failMode === 'closed' && config.enforcement === 'enforce') {
|
|
151
|
+
return { block: true, blockReason: 'SafeClaw service unavailable (fail-closed)' };
|
|
152
|
+
} else if (r === null && config.failMode === 'closed' && config.enforcement === 'warn-only') {
|
|
153
|
+
console.warn('[SafeClaw] Service unavailable (fail-closed mode, warn-only)');
|
|
154
|
+
}
|
|
155
|
+
if (r?.block) {
|
|
156
|
+
if (config.enforcement === 'enforce') {
|
|
157
|
+
return { block: true, blockReason: r.reason as string };
|
|
158
|
+
}
|
|
159
|
+
if (config.enforcement === 'warn-only') {
|
|
160
|
+
console.warn(`[SafeClaw] Warning: ${r.reason}`);
|
|
161
|
+
}
|
|
162
|
+
// audit-only: logged server-side, no action here
|
|
163
|
+
}
|
|
164
|
+
}, { priority: 100 });
|
|
165
|
+
|
|
166
|
+
// Context injection — prepend governance context to agent system prompt
|
|
167
|
+
api.on('before_agent_start', async (event: PluginEvent, ctx: PluginContext) => {
|
|
168
|
+
const r = await post('/context/build', {
|
|
169
|
+
sessionId: ctx.sessionId ?? event.sessionId,
|
|
170
|
+
userId: ctx.userId ?? event.userId,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (r?.prependContext) {
|
|
174
|
+
return { prependContext: r.prependContext as string };
|
|
175
|
+
}
|
|
176
|
+
}, { priority: 100 });
|
|
177
|
+
|
|
178
|
+
// Message governance — check outbound messages
|
|
179
|
+
api.on('message_sending', async (event: PluginEvent, ctx: PluginContext) => {
|
|
180
|
+
const r = await post('/evaluate/message', {
|
|
181
|
+
sessionId: ctx.sessionId ?? event.sessionId,
|
|
182
|
+
userId: ctx.userId ?? event.userId,
|
|
183
|
+
to: event.to,
|
|
184
|
+
content: event.content,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (r === null && config.failMode === 'closed' && config.enforcement === 'enforce') {
|
|
188
|
+
return { cancel: true };
|
|
189
|
+
} else if (r === null && config.failMode === 'closed' && config.enforcement === 'warn-only') {
|
|
190
|
+
console.warn('[SafeClaw] Service unavailable (fail-closed mode, warn-only)');
|
|
191
|
+
}
|
|
192
|
+
if (r?.block && config.enforcement === 'enforce') {
|
|
193
|
+
return { cancel: true };
|
|
194
|
+
}
|
|
195
|
+
}, { priority: 100 });
|
|
196
|
+
|
|
197
|
+
// Async logging — fire-and-forget, no return value needed
|
|
198
|
+
api.on('llm_input', (event: PluginEvent, ctx: PluginContext) => {
|
|
199
|
+
post('/log/llm-input', {
|
|
200
|
+
sessionId: ctx.sessionId ?? event.sessionId,
|
|
201
|
+
content: event.content,
|
|
202
|
+
}).catch(() => {});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
api.on('llm_output', (event: PluginEvent, ctx: PluginContext) => {
|
|
206
|
+
post('/log/llm-output', {
|
|
207
|
+
sessionId: ctx.sessionId ?? event.sessionId,
|
|
208
|
+
content: event.content,
|
|
209
|
+
}).catch(() => {});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
api.on('after_tool_call', (event: PluginEvent, ctx: PluginContext) => {
|
|
213
|
+
post('/record/tool-result', {
|
|
214
|
+
sessionId: ctx.sessionId ?? event.sessionId,
|
|
215
|
+
toolName: event.toolName ?? event.tool_name,
|
|
216
|
+
params: event.params ?? {},
|
|
217
|
+
result: event.result ?? '',
|
|
218
|
+
success: event.success ?? true,
|
|
219
|
+
}).catch(() => {});
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-safeclaw-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "SafeClaw Neurosymbolic Governance plugin for OpenClaw — validates AI agent actions against OWL ontologies and SHACL constraints",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"typecheck": "tsc --noEmit",
|
|
11
|
+
"prepublishOnly": "npm run build"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist/",
|
|
15
|
+
"index.ts",
|
|
16
|
+
"SKILL.md",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"openclaw",
|
|
21
|
+
"safeclaw",
|
|
22
|
+
"governance",
|
|
23
|
+
"neurosymbolic",
|
|
24
|
+
"ai-safety",
|
|
25
|
+
"owl",
|
|
26
|
+
"shacl",
|
|
27
|
+
"ontology",
|
|
28
|
+
"ai-agent",
|
|
29
|
+
"tool-validation"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/tendlyeu/SafeClaw.git",
|
|
35
|
+
"directory": "openclaw-safeclaw-plugin"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://safeclaw.eu",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/tendlyeu/SafeClaw/issues"
|
|
40
|
+
},
|
|
41
|
+
"author": "Tendly EU",
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"typescript": "^5.4.0",
|
|
44
|
+
"@types/node": "^20.0.0"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18.0.0"
|
|
48
|
+
}
|
|
49
|
+
}
|