daniel-ai-permissions-layer 0.2.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/LICENSE +26 -0
- package/README.md +302 -0
- package/dist/adapters/openai-adapter.d.ts +2 -0
- package/dist/adapters/openai-adapter.js +19 -0
- package/dist/adapters/openclaw-adapter.d.ts +6 -0
- package/dist/adapters/openclaw-adapter.js +202 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +52 -0
- package/dist/compiler.d.ts +5 -0
- package/dist/compiler.js +17 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/llm-adapter.d.ts +3 -0
- package/dist/llm-adapter.js +1 -0
- package/dist/matcher.d.ts +5 -0
- package/dist/matcher.js +49 -0
- package/dist/middleware.d.ts +13 -0
- package/dist/middleware.js +34 -0
- package/dist/path-protection.d.ts +6 -0
- package/dist/path-protection.js +34 -0
- package/dist/rules.d.ts +2 -0
- package/dist/rules.js +8 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.js +1 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
AI Permissions Layer
|
|
2
|
+
Copyright (C) 2025
|
|
3
|
+
|
|
4
|
+
This program is free software: you can redistribute it and/or modify
|
|
5
|
+
it under the terms of the GNU General Public License as published by
|
|
6
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
(at your option) any later version.
|
|
8
|
+
|
|
9
|
+
This program is distributed in the hope that it will be useful,
|
|
10
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
GNU General Public License for more details.
|
|
13
|
+
|
|
14
|
+
You should have received a copy of the GNU General Public License
|
|
15
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
GNU GENERAL PUBLIC LICENSE
|
|
20
|
+
Version 3, 29 June 2007
|
|
21
|
+
|
|
22
|
+
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
23
|
+
Everyone is permitted to copy and distribute verbatim copies
|
|
24
|
+
of this license document, but changing it is not allowed.
|
|
25
|
+
|
|
26
|
+
Full license text: https://www.gnu.org/licenses/gpl-3.0.html
|
package/README.md
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# AI Permissions Layer
|
|
2
|
+
|
|
3
|
+
Middleware that intercepts AI agent tool calls, applies your rules, and returns **allow**, **block**, or **require approval**. Use it to control what your agent can do—block dangerous actions, require approval for sensitive ones, and allow safe operations by default.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Quick Start (OpenClaw)](#quick-start-openclaw)
|
|
10
|
+
- [Writing Rules](#writing-rules)
|
|
11
|
+
- [OpenClaw Setup & Usage](#openclaw-setup--usage)
|
|
12
|
+
- [Rule Reference](#rule-reference)
|
|
13
|
+
- [Approval Flow](#approval-flow)
|
|
14
|
+
- [Path Protection](#path-protection)
|
|
15
|
+
- [Configuration](#configuration)
|
|
16
|
+
- [Standalone Compile](#standalone-compile)
|
|
17
|
+
- [Library Usage](#library-usage)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Quick Start (OpenClaw)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
openclaw plugins install ai-permissions-openclaw
|
|
25
|
+
openclaw gateway restart
|
|
26
|
+
openclaw ai-permissions compile
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The first `compile` creates `~/.openclaw/rules.yaml` with starter rules if none exist. Edit `rules.yaml`, run `compile` again when you change rules, and you're done. No extra API keys needed if you've run `openclaw onboard`.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Writing Rules
|
|
34
|
+
|
|
35
|
+
Rules define what the agent can do. Each rule has an **action** and applies to one or more **tools**.
|
|
36
|
+
|
|
37
|
+
### Actions
|
|
38
|
+
|
|
39
|
+
| Action | Behavior |
|
|
40
|
+
|--------|----------|
|
|
41
|
+
| **block** | Tool is never allowed. The agent sees an error and cannot proceed. |
|
|
42
|
+
| **require_approval** | Tool is blocked until you reply `APPROVE <uuid>` in chat. One-time approval per request. |
|
|
43
|
+
| **allow** | Tool runs without asking. |
|
|
44
|
+
|
|
45
|
+
### Plain-Text Format (YAML)
|
|
46
|
+
|
|
47
|
+
Write rules in plain English, one per line, each starting with `-`:
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
# Block dangerous actions
|
|
51
|
+
- block gmail.delete and gmail.batchDelete - never auto-delete emails
|
|
52
|
+
- block payments and money transfers - no financial actions
|
|
53
|
+
|
|
54
|
+
# Require approval before risky operations
|
|
55
|
+
- require approval before exec, bash, or process - ask before running commands
|
|
56
|
+
- require approval before write, edit, apply_patch - ask before file changes
|
|
57
|
+
|
|
58
|
+
# Allow safe read-only operations
|
|
59
|
+
- allow read, search, list - safe read-only operations
|
|
60
|
+
- allow gmail.list and gmail.get - reading emails is fine
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Compile** these into JSON with `openclaw ai-permissions compile` or `npx ai-permissions-compile --openclaw`. The compiler uses an LLM to infer tool names from your wording.
|
|
64
|
+
|
|
65
|
+
### Compiler Language Hints
|
|
66
|
+
|
|
67
|
+
The compiler maps natural language to actions:
|
|
68
|
+
|
|
69
|
+
| You write | Action |
|
|
70
|
+
|-----------|--------|
|
|
71
|
+
| "block", "don't allow", "never" | `block` |
|
|
72
|
+
| "ask me", "prompt me", "require approval", "before X" | `require_approval` |
|
|
73
|
+
| "allow", "ok", "permit" | `allow` |
|
|
74
|
+
|
|
75
|
+
Include tool names when you know them (e.g. `gmail.delete`, `write`, `exec`). The compiler will infer others (e.g. "payments" → `payments`, "money transfers" → `money_transfers`).
|
|
76
|
+
|
|
77
|
+
### Compiled JSON Format
|
|
78
|
+
|
|
79
|
+
After compiling, rules become JSON:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"rules": [
|
|
84
|
+
{ "action": "block", "tool": "gmail.delete", "reason": "never auto-delete emails" },
|
|
85
|
+
{ "action": "require_approval", "tool": "write", "reason": "ask before file changes" },
|
|
86
|
+
{ "action": "allow", "tool": "read", "reason": "safe read-only operations" }
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Each rule: `action`, `tool`, `reason`. The plugin loads this file; you typically edit the YAML and recompile.
|
|
92
|
+
|
|
93
|
+
### OpenClaw Tool Names
|
|
94
|
+
|
|
95
|
+
Common OpenClaw tools to reference in rules:
|
|
96
|
+
|
|
97
|
+
| Category | Tools |
|
|
98
|
+
|----------|-------|
|
|
99
|
+
| **Files** | `read`, `write`, `edit`, `apply_patch` |
|
|
100
|
+
| **Runtime** | `exec`, `bash`, `process` |
|
|
101
|
+
| **Gmail** | `gmail.list`, `gmail.get`, `gmail.send`, `gmail.delete`, `gmail.batchDelete` |
|
|
102
|
+
| **Browser** | `browser_*`, `web_*` |
|
|
103
|
+
| **Internal** | `pairing`, `device-pair`, `openclaw.*` (these bypass rules) |
|
|
104
|
+
|
|
105
|
+
Internal tools (pairing, device-pair, openclaw.*) are never intercepted.
|
|
106
|
+
|
|
107
|
+
### Rule Precedence
|
|
108
|
+
|
|
109
|
+
- **block** wins over **require_approval** when both match the same tool.
|
|
110
|
+
- First matching rule applies. Order matters when you have overlapping rules.
|
|
111
|
+
- When **no rule matches**, behavior is set by `defaultWhenNoMatch` (default: `require_approval`).
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## OpenClaw Setup & Usage
|
|
116
|
+
|
|
117
|
+
### Installation
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
openclaw plugins install ai-permissions-openclaw
|
|
121
|
+
openclaw gateway restart
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Compile Rules
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# Default: ~/.openclaw/rules.yaml → ~/.openclaw/ai-permissions-rules.json
|
|
128
|
+
openclaw ai-permissions compile
|
|
129
|
+
|
|
130
|
+
# Custom input
|
|
131
|
+
openclaw ai-permissions compile my-rules.yaml
|
|
132
|
+
|
|
133
|
+
# Custom input and output
|
|
134
|
+
openclaw ai-permissions compile my-rules.yaml ~/.openclaw/ai-permissions-rules.json
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**First run:** If `~/.openclaw/rules.yaml` doesn't exist, it is created with starter rules and compiled.
|
|
138
|
+
|
|
139
|
+
**Credentials:** Uses OpenClaw's primary model from `~/.openclaw/openclaw.json` and credentials from `~/.openclaw/.env`. Run `openclaw onboard` first if you haven't.
|
|
140
|
+
|
|
141
|
+
### Workflow
|
|
142
|
+
|
|
143
|
+
1. Edit `~/.openclaw/rules.yaml`
|
|
144
|
+
2. Run `openclaw ai-permissions compile`
|
|
145
|
+
3. Rules take effect immediately (plugin reloads on each tool call)
|
|
146
|
+
4. Restart the gateway only when changing plugin config
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Rule Reference
|
|
151
|
+
|
|
152
|
+
### Exact Tool Match
|
|
153
|
+
|
|
154
|
+
Rules match by exact tool name:
|
|
155
|
+
|
|
156
|
+
```yaml
|
|
157
|
+
- block gmail.delete - no auto delete
|
|
158
|
+
- allow read - read-only ok
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Multiple Tools per Rule
|
|
162
|
+
|
|
163
|
+
List tools in one rule (compiler creates one JSON rule per tool):
|
|
164
|
+
|
|
165
|
+
```yaml
|
|
166
|
+
- block gmail.delete and gmail.batchDelete - never auto-delete emails
|
|
167
|
+
- require approval before write, edit, apply_patch - ask before file changes
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Advanced: toolPattern and intentPattern (JSON only)
|
|
171
|
+
|
|
172
|
+
For programmatic use, compiled rules can include:
|
|
173
|
+
|
|
174
|
+
- **toolPattern** — regex to match tool names (e.g. `gmail\.(delete|batchDelete)`)
|
|
175
|
+
- **intentPattern** — regex to match user intent text (optional)
|
|
176
|
+
|
|
177
|
+
These are produced by the compiler when it infers patterns. You can also edit the JSON directly for fine-grained control.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Approval Flow
|
|
182
|
+
|
|
183
|
+
When a tool needs approval:
|
|
184
|
+
|
|
185
|
+
1. The agent attempts the tool.
|
|
186
|
+
2. The plugin blocks it and returns a message with a **request ID** (UUID).
|
|
187
|
+
3. You reply in chat: `APPROVE <uuid>` to allow once, or `DENY <uuid>` to block.
|
|
188
|
+
4. If you approved, the agent can retry the same action; the approval is consumed.
|
|
189
|
+
|
|
190
|
+
**Example:**
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
Agent: I'll run `write` to save the file.
|
|
194
|
+
[Blocked] [Approval required] ask before file changes
|
|
195
|
+
Request ID: a1b2c3d4-...
|
|
196
|
+
Reply APPROVE a1b2c3d4-... to allow, or DENY a1b2c3d4-... to block.
|
|
197
|
+
|
|
198
|
+
You: APPROVE a1b2c3d4-...
|
|
199
|
+
|
|
200
|
+
Agent: [retries the write; it succeeds]
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Path Protection
|
|
206
|
+
|
|
207
|
+
The plugin blocks the agent from modifying its own rules file. Writes to paths matching `**/rules*.json` or `**/.config/ai-permissions-layer/**` via `write`, `edit`, or `apply_patch` are blocked regardless of your rules.
|
|
208
|
+
|
|
209
|
+
This is enabled by default. Disable or customize via `pathProtection` in plugin config.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Configuration
|
|
214
|
+
|
|
215
|
+
Edit `~/.openclaw/openclaw.json` under `plugins.entries.ai-permissions-openclaw.config`:
|
|
216
|
+
|
|
217
|
+
| Option | Default | Description |
|
|
218
|
+
|--------|---------|--------------|
|
|
219
|
+
| `rulesPath` | `~/.openclaw/ai-permissions-rules.json` | Path to compiled rules JSON |
|
|
220
|
+
| `defaultWhenNoMatch` | `require_approval` | When no rule matches: `allow`, `require_approval`, or `block` |
|
|
221
|
+
| `pathProtection.enabled` | `true` | Block writes to rules file |
|
|
222
|
+
| `pathProtection.dangerousTools` | `['write','edit','apply_patch']` | Tools that can write files (for path protection) |
|
|
223
|
+
|
|
224
|
+
**Example:**
|
|
225
|
+
|
|
226
|
+
```json
|
|
227
|
+
{
|
|
228
|
+
"plugins": {
|
|
229
|
+
"entries": {
|
|
230
|
+
"ai-permissions-openclaw": {
|
|
231
|
+
"enabled": true,
|
|
232
|
+
"config": {
|
|
233
|
+
"rulesPath": "~/.openclaw/ai-permissions-rules.json",
|
|
234
|
+
"defaultWhenNoMatch": "require_approval",
|
|
235
|
+
"pathProtection": {
|
|
236
|
+
"enabled": true
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Standalone Compile
|
|
248
|
+
|
|
249
|
+
Without OpenClaw, use the npm CLI:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
# With OpenAI (requires OPENAI_API_KEY)
|
|
253
|
+
export OPENAI_API_KEY=your_key
|
|
254
|
+
npx ai-permissions-compile examples/rules.yaml ~/.openclaw/ai-permissions-rules.json
|
|
255
|
+
|
|
256
|
+
# With OpenClaw config (uses ~/.openclaw/.env and openclaw.json)
|
|
257
|
+
npx ai-permissions-compile --openclaw examples/rules.yaml ~/.openclaw/ai-permissions-rules.json
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**Format:** One rule per line, each starting with `-`. See [examples/rules.yaml](examples/rules.yaml).
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Library Usage
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
npm install ai-permissions-layer
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
import { createMiddleware, match } from 'ai-permissions-layer';
|
|
272
|
+
|
|
273
|
+
const rules = [
|
|
274
|
+
{ action: 'block', tool: 'gmail.delete', reason: 'no delete' },
|
|
275
|
+
{ action: 'require_approval', tool: 'gmail.send', reason: 'ask first' },
|
|
276
|
+
{ action: 'allow', tool: 'read', reason: 'read-only ok' },
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
const middleware = createMiddleware(rules, executor, {
|
|
280
|
+
defaultWhenNoMatch: 'require_approval',
|
|
281
|
+
pathProtection: {},
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const result = await middleware(
|
|
285
|
+
{ toolName: 'gmail.delete', args: {} },
|
|
286
|
+
{ text: 'delete emails' }
|
|
287
|
+
);
|
|
288
|
+
// result.decision === 'BLOCK', result.executed === false
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Examples
|
|
294
|
+
|
|
295
|
+
- [examples/rules.yaml](examples/rules.yaml) — plain-text rules
|
|
296
|
+
- [examples/ai-permissions-rules.json](examples/ai-permissions-rules.json) — compiled JSON
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## License
|
|
301
|
+
|
|
302
|
+
GPL v3.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function createOpenAIAdapter(apiKey, model = 'gpt-4o-mini') {
|
|
2
|
+
return {
|
|
3
|
+
async complete(prompt) {
|
|
4
|
+
const res = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
5
|
+
method: 'POST',
|
|
6
|
+
headers: {
|
|
7
|
+
'Content-Type': 'application/json',
|
|
8
|
+
Authorization: `Bearer ${apiKey}`,
|
|
9
|
+
},
|
|
10
|
+
body: JSON.stringify({
|
|
11
|
+
model,
|
|
12
|
+
messages: [{ role: 'user', content: prompt }],
|
|
13
|
+
}),
|
|
14
|
+
});
|
|
15
|
+
const data = (await res.json());
|
|
16
|
+
return data.choices[0].message.content;
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { LLMAdapter } from '../llm-adapter.js';
|
|
2
|
+
/**
|
|
3
|
+
* Create an LLM adapter that uses OpenClaw's primary model from ~/.openclaw/openclaw.json.
|
|
4
|
+
* Falls back to OpenAI if config is missing or model resolution fails.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createOpenClawAdapter(fallbackApiKey?: string): LLMAdapter | null;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
function resolvePath(p) {
|
|
5
|
+
if (p.startsWith('~'))
|
|
6
|
+
return path.join(os.homedir(), p.slice(1));
|
|
7
|
+
return path.resolve(p);
|
|
8
|
+
}
|
|
9
|
+
function loadOpenClawEnv() {
|
|
10
|
+
const envPath = resolvePath('~/.openclaw/.env');
|
|
11
|
+
if (!existsSync(envPath))
|
|
12
|
+
return;
|
|
13
|
+
try {
|
|
14
|
+
const content = readFileSync(envPath, 'utf-8');
|
|
15
|
+
for (const line of content.split('\n')) {
|
|
16
|
+
const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
17
|
+
if (m && !(m[1] in process.env)) {
|
|
18
|
+
const val = m[2].replace(/^["']|["']$/g, '').trim();
|
|
19
|
+
process.env[m[1]] = val;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
/* ignore */
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function loadOpenClawConfig() {
|
|
28
|
+
const paths = [
|
|
29
|
+
resolvePath('~/.openclaw/openclaw.json'),
|
|
30
|
+
resolvePath('~/.config/openclaw/openclaw.json'),
|
|
31
|
+
];
|
|
32
|
+
for (const p of paths) {
|
|
33
|
+
if (existsSync(p)) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(readFileSync(p, 'utf-8'));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
function resolveEnvValue(val) {
|
|
45
|
+
if (typeof val !== 'string')
|
|
46
|
+
return undefined;
|
|
47
|
+
const m = val.match(/^\$\{?([A-Z_][A-Z0-9_]*)\}?$/);
|
|
48
|
+
if (m)
|
|
49
|
+
return process.env[m[1]];
|
|
50
|
+
return val;
|
|
51
|
+
}
|
|
52
|
+
const BUILTIN_PROVIDERS = {
|
|
53
|
+
openai: {
|
|
54
|
+
baseUrl: 'https://api.openai.com/v1',
|
|
55
|
+
getApiKey: () => process.env.OPENAI_API_KEY,
|
|
56
|
+
},
|
|
57
|
+
ollama: {
|
|
58
|
+
baseUrl: 'http://127.0.0.1:11434/v1',
|
|
59
|
+
getApiKey: () => 'ollama',
|
|
60
|
+
},
|
|
61
|
+
'lm-studio': {
|
|
62
|
+
baseUrl: 'http://localhost:1234/v1',
|
|
63
|
+
getApiKey: () => 'lm-studio',
|
|
64
|
+
},
|
|
65
|
+
lmstudio: {
|
|
66
|
+
baseUrl: 'http://localhost:1234/v1',
|
|
67
|
+
getApiKey: () => 'lm-studio',
|
|
68
|
+
},
|
|
69
|
+
vllm: {
|
|
70
|
+
baseUrl: 'http://127.0.0.1:8000/v1',
|
|
71
|
+
getApiKey: () => '',
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Create an LLM adapter that uses OpenClaw's primary model from ~/.openclaw/openclaw.json.
|
|
76
|
+
* Falls back to OpenAI if config is missing or model resolution fails.
|
|
77
|
+
*/
|
|
78
|
+
export function createOpenClawAdapter(fallbackApiKey) {
|
|
79
|
+
loadOpenClawEnv();
|
|
80
|
+
const config = loadOpenClawConfig();
|
|
81
|
+
if (!config)
|
|
82
|
+
return null;
|
|
83
|
+
const agents = (config.agents ?? config.agent);
|
|
84
|
+
const defaults = agents?.defaults;
|
|
85
|
+
const modelConfig = (defaults?.model ?? agents?.model);
|
|
86
|
+
const primary = modelConfig?.primary;
|
|
87
|
+
if (!primary || typeof primary !== 'string')
|
|
88
|
+
return null;
|
|
89
|
+
const slash = primary.indexOf('/');
|
|
90
|
+
const provider = slash >= 0 ? primary.slice(0, slash) : 'openai';
|
|
91
|
+
const modelId = slash >= 0 ? primary.slice(slash + 1) : primary;
|
|
92
|
+
const models = config.models;
|
|
93
|
+
const providers = models?.providers;
|
|
94
|
+
const providerConfig = providers?.[provider];
|
|
95
|
+
let baseUrl;
|
|
96
|
+
let apiKey;
|
|
97
|
+
if (providerConfig?.baseUrl) {
|
|
98
|
+
baseUrl = providerConfig.baseUrl.replace(/\/$/, '');
|
|
99
|
+
if (!baseUrl.endsWith('/v1'))
|
|
100
|
+
baseUrl += '/v1';
|
|
101
|
+
apiKey = resolveEnvValue(providerConfig.apiKey) ?? providerConfig.apiKey;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const builtin = BUILTIN_PROVIDERS[provider] ?? BUILTIN_PROVIDERS.openai;
|
|
105
|
+
baseUrl = builtin.baseUrl;
|
|
106
|
+
apiKey = builtin.getApiKey();
|
|
107
|
+
}
|
|
108
|
+
if (!apiKey && provider === 'openai') {
|
|
109
|
+
apiKey = fallbackApiKey ?? process.env.OPENAI_API_KEY;
|
|
110
|
+
}
|
|
111
|
+
if (!apiKey && provider !== 'ollama' && provider !== 'lmstudio' && provider !== 'lm-studio' && provider !== 'vllm') {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const chatUrl = `${baseUrl}/chat/completions`;
|
|
115
|
+
const completionsUrl = `${baseUrl}/completions`;
|
|
116
|
+
const headers = {
|
|
117
|
+
'Content-Type': 'application/json',
|
|
118
|
+
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
|
119
|
+
};
|
|
120
|
+
async function tryChatWithModel(model, p) {
|
|
121
|
+
const res = await fetch(chatUrl, {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers,
|
|
124
|
+
body: JSON.stringify({
|
|
125
|
+
model,
|
|
126
|
+
messages: [{ role: 'user', content: p }],
|
|
127
|
+
}),
|
|
128
|
+
});
|
|
129
|
+
if (!res.ok) {
|
|
130
|
+
const err = await res.text();
|
|
131
|
+
throw new Error(`OpenClaw model request failed: ${res.status} ${err}`);
|
|
132
|
+
}
|
|
133
|
+
const data = (await res.json());
|
|
134
|
+
const content = data.choices?.[0]?.message?.content;
|
|
135
|
+
if (content == null)
|
|
136
|
+
throw new Error('No response content from model');
|
|
137
|
+
return content;
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
async complete(prompt) {
|
|
141
|
+
const tryChat = async () => {
|
|
142
|
+
const res = await fetch(chatUrl, {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
headers,
|
|
145
|
+
body: JSON.stringify({
|
|
146
|
+
model: modelId,
|
|
147
|
+
messages: [{ role: 'user', content: prompt }],
|
|
148
|
+
}),
|
|
149
|
+
});
|
|
150
|
+
if (!res.ok) {
|
|
151
|
+
const err = await res.text();
|
|
152
|
+
if (res.status === 404 && /not a chat model|chat\/completions/i.test(err)) {
|
|
153
|
+
throw new Error('USE_COMPLETIONS');
|
|
154
|
+
}
|
|
155
|
+
throw new Error(`OpenClaw model request failed: ${res.status} ${err}`);
|
|
156
|
+
}
|
|
157
|
+
const data = (await res.json());
|
|
158
|
+
const content = data.choices?.[0]?.message?.content;
|
|
159
|
+
if (content == null)
|
|
160
|
+
throw new Error('No response content from model');
|
|
161
|
+
return content;
|
|
162
|
+
};
|
|
163
|
+
const tryCompletions = async () => {
|
|
164
|
+
const res = await fetch(completionsUrl, {
|
|
165
|
+
method: 'POST',
|
|
166
|
+
headers,
|
|
167
|
+
body: JSON.stringify({
|
|
168
|
+
model: modelId,
|
|
169
|
+
prompt,
|
|
170
|
+
}),
|
|
171
|
+
});
|
|
172
|
+
if (!res.ok) {
|
|
173
|
+
const err = await res.text();
|
|
174
|
+
throw new Error(`OpenClaw model request failed: ${res.status} ${err}`);
|
|
175
|
+
}
|
|
176
|
+
const data = (await res.json());
|
|
177
|
+
const text = data.choices?.[0]?.text;
|
|
178
|
+
if (text == null)
|
|
179
|
+
throw new Error('No response content from model');
|
|
180
|
+
return text.trim();
|
|
181
|
+
};
|
|
182
|
+
try {
|
|
183
|
+
return await tryChat();
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
if (e instanceof Error && e.message === 'USE_COMPLETIONS') {
|
|
187
|
+
try {
|
|
188
|
+
return await tryCompletions();
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
/* fall through to chat fallback */
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (provider === 'openai' && apiKey) {
|
|
195
|
+
console.error(`Primary model ${modelId} does not support chat; using gpt-4o-mini for compile`);
|
|
196
|
+
return await tryChatWithModel('gpt-4o-mini', prompt);
|
|
197
|
+
}
|
|
198
|
+
throw e;
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
3
|
+
import { compile } from './compiler.js';
|
|
4
|
+
import { createOpenAIAdapter } from './adapters/openai-adapter.js';
|
|
5
|
+
import { createOpenClawAdapter } from './adapters/openclaw-adapter.js';
|
|
6
|
+
async function main() {
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const useOpenClaw = args.includes('--openclaw');
|
|
9
|
+
const filtered = args.filter((a) => a !== '--openclaw');
|
|
10
|
+
const inputFile = filtered[0] || 'rules.yaml';
|
|
11
|
+
const outputFile = filtered[1] || 'rules.compiled.json';
|
|
12
|
+
let llm;
|
|
13
|
+
if (useOpenClaw) {
|
|
14
|
+
const openClaw = createOpenClawAdapter(process.env.OPENAI_API_KEY);
|
|
15
|
+
if (openClaw) {
|
|
16
|
+
llm = openClaw;
|
|
17
|
+
console.error('Using OpenClaw primary model');
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
21
|
+
if (!apiKey) {
|
|
22
|
+
console.error('OpenClaw config not found or model unresolved. Set OPENAI_API_KEY or run openclaw onboard first.');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
llm = createOpenAIAdapter(apiKey);
|
|
26
|
+
console.error('OpenClaw config not found, falling back to OpenAI');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
31
|
+
if (!apiKey) {
|
|
32
|
+
console.error('OPENAI_API_KEY required');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
llm = createOpenAIAdapter(apiKey);
|
|
36
|
+
}
|
|
37
|
+
if (!existsSync(inputFile)) {
|
|
38
|
+
console.error(`Input file not found: ${inputFile}`);
|
|
39
|
+
console.error('Usage: npx ai-permissions-compile [--openclaw] <input.yaml> [output.json]');
|
|
40
|
+
console.error('Example: npx ai-permissions-compile --openclaw examples/rules.yaml ~/.openclaw/ai-permissions-rules.json');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
const content = readFileSync(inputFile, 'utf-8');
|
|
44
|
+
const rules = content
|
|
45
|
+
.split('\n')
|
|
46
|
+
.filter((l) => l.trim().startsWith('-'))
|
|
47
|
+
.map((l) => l.replace(/^-\s*["']?|["']?$/g, '').trim());
|
|
48
|
+
const { rules: compiled } = await compile(rules, llm);
|
|
49
|
+
writeFileSync(outputFile, JSON.stringify({ rules: compiled }, null, 2));
|
|
50
|
+
console.log(`Compiled ${compiled.length} rules to ${outputFile}`);
|
|
51
|
+
}
|
|
52
|
+
main().catch(console.error);
|
package/dist/compiler.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const COMPILER_PROMPT = `You are a rule extractor. Convert user rules into structured JSON.
|
|
2
|
+
|
|
3
|
+
Rules:
|
|
4
|
+
- "don't allow" / "never" / "block" → action: "block"
|
|
5
|
+
- "ask me" / "prompt me" / "before X" → action: "require_approval" (NEVER "allow")
|
|
6
|
+
- "allow" → action: "allow"
|
|
7
|
+
|
|
8
|
+
Output ONLY valid JSON: { "rules": [ { "action": "...", "tool": "...", "reason": "..." } ] }
|
|
9
|
+
Include tool names when inferable (e.g. gmail.delete, gmail.batchDelete for email delete).
|
|
10
|
+
`;
|
|
11
|
+
export async function compile(plainTextRules, llm) {
|
|
12
|
+
const prompt = `${COMPILER_PROMPT}\n\nUser rules:\n${plainTextRules.map((r) => `- ${r}`).join('\n')}`;
|
|
13
|
+
const raw = await llm.complete(prompt);
|
|
14
|
+
const stripped = raw.replace(/^```(?:json)?\s*\n?|\n?```\s*$/g, '').trim();
|
|
15
|
+
const parsed = JSON.parse(stripped);
|
|
16
|
+
return { rules: parsed.rules };
|
|
17
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './matcher.js';
|
|
3
|
+
export * from './compiler.js';
|
|
4
|
+
export * from './middleware.js';
|
|
5
|
+
export * from './llm-adapter.js';
|
|
6
|
+
export * from './rules.js';
|
|
7
|
+
export * from './path-protection.js';
|
|
8
|
+
export { createOpenClawAdapter } from './adapters/openclaw-adapter.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './matcher.js';
|
|
3
|
+
export * from './compiler.js';
|
|
4
|
+
export * from './middleware.js';
|
|
5
|
+
export * from './llm-adapter.js';
|
|
6
|
+
export * from './rules.js';
|
|
7
|
+
export * from './path-protection.js';
|
|
8
|
+
export { createOpenClawAdapter } from './adapters/openclaw-adapter.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ToolCall, Intent, CompiledRule, CheckResult, DefaultWhenNoMatch } from './types.js';
|
|
2
|
+
export interface MatchOptions {
|
|
3
|
+
defaultWhenNoMatch?: DefaultWhenNoMatch;
|
|
4
|
+
}
|
|
5
|
+
export declare function match(toolCall: ToolCall, intent: Intent, rules: CompiledRule[], options?: MatchOptions): CheckResult;
|
package/dist/matcher.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const ACTION_TO_DECISION = {
|
|
2
|
+
block: 'BLOCK',
|
|
3
|
+
require_approval: 'REQUIRES_APPROVAL',
|
|
4
|
+
allow: 'ALLOW',
|
|
5
|
+
};
|
|
6
|
+
function matchArgs(rule, toolCall) {
|
|
7
|
+
if (!rule.argsPattern)
|
|
8
|
+
return true;
|
|
9
|
+
const argsStr = Object.values(toolCall.args)
|
|
10
|
+
.filter((v) => typeof v === 'string' || typeof v === 'number')
|
|
11
|
+
.join(' ');
|
|
12
|
+
return new RegExp(rule.argsPattern).test(argsStr);
|
|
13
|
+
}
|
|
14
|
+
export function match(toolCall, intent, rules, options = {}) {
|
|
15
|
+
const { defaultWhenNoMatch = 'require_approval' } = options;
|
|
16
|
+
let matched = null;
|
|
17
|
+
let matchedAction = null;
|
|
18
|
+
for (const rule of rules) {
|
|
19
|
+
const toolMatch = (rule.tool && rule.tool === toolCall.toolName) ||
|
|
20
|
+
(rule.toolPattern &&
|
|
21
|
+
new RegExp(rule.toolPattern).test(toolCall.toolName));
|
|
22
|
+
const argsMatch = matchArgs(rule, toolCall);
|
|
23
|
+
const intentMatch = !rule.intentPattern ||
|
|
24
|
+
new RegExp(rule.intentPattern, 'i').test(intent.text);
|
|
25
|
+
if (toolMatch && argsMatch && intentMatch) {
|
|
26
|
+
if (!matched || rule.action === 'block') {
|
|
27
|
+
matched = rule;
|
|
28
|
+
matchedAction = rule.action;
|
|
29
|
+
if (rule.action === 'block')
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (!matched) {
|
|
35
|
+
const decision = defaultWhenNoMatch === 'allow'
|
|
36
|
+
? 'ALLOW'
|
|
37
|
+
: defaultWhenNoMatch === 'block'
|
|
38
|
+
? 'BLOCK'
|
|
39
|
+
: 'REQUIRES_APPROVAL';
|
|
40
|
+
return {
|
|
41
|
+
decision,
|
|
42
|
+
reason: 'No matching rule',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
decision: ACTION_TO_DECISION[matchedAction],
|
|
47
|
+
reason: matched.reason,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ToolCall, Intent, CompiledRule, DefaultWhenNoMatch, PathProtectionConfig } from './types.js';
|
|
2
|
+
export type ToolExecutor = (toolCall: ToolCall) => Promise<unknown>;
|
|
3
|
+
export interface MiddlewareResult {
|
|
4
|
+
decision: 'ALLOW' | 'BLOCK' | 'REQUIRES_APPROVAL';
|
|
5
|
+
reason?: string;
|
|
6
|
+
executed: boolean;
|
|
7
|
+
result?: unknown;
|
|
8
|
+
}
|
|
9
|
+
export interface MiddlewareOptions {
|
|
10
|
+
defaultWhenNoMatch?: DefaultWhenNoMatch;
|
|
11
|
+
pathProtection?: PathProtectionConfig | Record<string, never>;
|
|
12
|
+
}
|
|
13
|
+
export declare function createMiddleware(rules: CompiledRule[], executor: ToolExecutor, options?: MiddlewareOptions): (toolCall: ToolCall, intent: Intent) => Promise<MiddlewareResult>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { match } from './matcher.js';
|
|
2
|
+
import { isProtectedPathViolation, DEFAULT_DANGEROUS_TOOLS, DEFAULT_PROTECTED_PATTERNS, } from './path-protection.js';
|
|
3
|
+
function resolvePathProtection(pathProtection) {
|
|
4
|
+
if (!pathProtection)
|
|
5
|
+
return null;
|
|
6
|
+
if (Object.keys(pathProtection).length === 0) {
|
|
7
|
+
return {
|
|
8
|
+
dangerousTools: DEFAULT_DANGEROUS_TOOLS,
|
|
9
|
+
protectedPatterns: DEFAULT_PROTECTED_PATTERNS,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
return pathProtection;
|
|
13
|
+
}
|
|
14
|
+
export function createMiddleware(rules, executor, options = {}) {
|
|
15
|
+
const { defaultWhenNoMatch = 'require_approval', pathProtection, } = options;
|
|
16
|
+
const pathConfig = resolvePathProtection(pathProtection);
|
|
17
|
+
return async (toolCall, intent) => {
|
|
18
|
+
if (pathConfig && isProtectedPathViolation(toolCall, pathConfig)) {
|
|
19
|
+
return {
|
|
20
|
+
decision: 'BLOCK',
|
|
21
|
+
reason: 'Protected path: rules cannot be modified by agent',
|
|
22
|
+
executed: false,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const { decision, reason } = match(toolCall, intent, rules, {
|
|
26
|
+
defaultWhenNoMatch,
|
|
27
|
+
});
|
|
28
|
+
if (decision === 'BLOCK' || decision === 'REQUIRES_APPROVAL') {
|
|
29
|
+
return { decision, reason, executed: false };
|
|
30
|
+
}
|
|
31
|
+
const result = await executor(toolCall);
|
|
32
|
+
return { decision: 'ALLOW', executed: true, result };
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ToolCall, PathProtectionConfig } from './types.js';
|
|
2
|
+
export declare const DEFAULT_DANGEROUS_TOOLS: string[];
|
|
3
|
+
/** OpenClaw file tools (group:fs) that can write files */
|
|
4
|
+
export declare const OPENCLAW_DANGEROUS_TOOLS: string[];
|
|
5
|
+
export declare const DEFAULT_PROTECTED_PATTERNS: string[];
|
|
6
|
+
export declare function isProtectedPathViolation(toolCall: ToolCall, config: PathProtectionConfig): boolean;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { minimatch } from 'minimatch';
|
|
2
|
+
export const DEFAULT_DANGEROUS_TOOLS = [
|
|
3
|
+
'filesystem.write',
|
|
4
|
+
'filesystem.edit',
|
|
5
|
+
'write_file',
|
|
6
|
+
'edit_file',
|
|
7
|
+
'writeFile',
|
|
8
|
+
'fs.writeFile',
|
|
9
|
+
];
|
|
10
|
+
/** OpenClaw file tools (group:fs) that can write files */
|
|
11
|
+
export const OPENCLAW_DANGEROUS_TOOLS = ['write', 'edit', 'apply_patch'];
|
|
12
|
+
export const DEFAULT_PROTECTED_PATTERNS = [
|
|
13
|
+
'**/rules*.json',
|
|
14
|
+
'**/.config/ai-permissions-layer/**',
|
|
15
|
+
];
|
|
16
|
+
const PATH_KEYS = ['path', 'file_path', 'filePath', 'filename'];
|
|
17
|
+
function extractPath(args) {
|
|
18
|
+
for (const key of PATH_KEYS) {
|
|
19
|
+
const val = args[key];
|
|
20
|
+
if (typeof val === 'string')
|
|
21
|
+
return val;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
export function isProtectedPathViolation(toolCall, config) {
|
|
26
|
+
if (!config.dangerousTools.includes(toolCall.toolName)) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
const path = extractPath(toolCall.args);
|
|
30
|
+
if (!path)
|
|
31
|
+
return false;
|
|
32
|
+
const normalizedPath = path.replace(/\\/g, '/');
|
|
33
|
+
return config.protectedPatterns.some((pattern) => minimatch(normalizedPath, pattern));
|
|
34
|
+
}
|
package/dist/rules.d.ts
ADDED
package/dist/rules.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type Decision = 'ALLOW' | 'BLOCK' | 'REQUIRES_APPROVAL';
|
|
2
|
+
export interface ToolCall {
|
|
3
|
+
toolName: string;
|
|
4
|
+
args: Record<string, unknown>;
|
|
5
|
+
}
|
|
6
|
+
export interface Intent {
|
|
7
|
+
text: string;
|
|
8
|
+
}
|
|
9
|
+
export type RuleAction = 'block' | 'require_approval' | 'allow';
|
|
10
|
+
export interface CompiledRule {
|
|
11
|
+
action: RuleAction;
|
|
12
|
+
tool?: string;
|
|
13
|
+
toolPattern?: string;
|
|
14
|
+
intentPattern?: string;
|
|
15
|
+
/** Regex to match against tool call arguments (joined as space-separated string) */
|
|
16
|
+
argsPattern?: string;
|
|
17
|
+
reason: string;
|
|
18
|
+
}
|
|
19
|
+
export interface CheckResult {
|
|
20
|
+
decision: Decision;
|
|
21
|
+
reason?: string;
|
|
22
|
+
}
|
|
23
|
+
/** When no rule matches: 'allow', 'require_approval', or 'block'. Default: 'require_approval'. */
|
|
24
|
+
export type DefaultWhenNoMatch = 'allow' | 'require_approval' | 'block';
|
|
25
|
+
export interface PathProtectionConfig {
|
|
26
|
+
/** Tool names that can write files (e.g. "filesystem.write", "edit_file") */
|
|
27
|
+
dangerousTools: string[];
|
|
28
|
+
/** Glob patterns for paths the agent must not write to */
|
|
29
|
+
protectedPatterns: string[];
|
|
30
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "daniel-ai-permissions-layer",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Offline-first middleware for AI agent tool call permissions. Intercepts tool calls, applies rules, returns ALLOW | BLOCK | REQUIRES_APPROVAL.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"ai-permissions-compile": "./dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"test:watch": "vitest",
|
|
15
|
+
"build:plugin": "npm run build && cd openclaw-plugin && npm run build",
|
|
16
|
+
"prepublishOnly": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"LICENSE",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/wei292224644/my-ai-permissions-layer.git"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/wei292224644/my-ai-permissions-layer/issues"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/wei292224644/my-ai-permissions-layer#readme",
|
|
31
|
+
"keywords": [
|
|
32
|
+
"ai",
|
|
33
|
+
"permissions",
|
|
34
|
+
"middleware",
|
|
35
|
+
"openclaw",
|
|
36
|
+
"tool-calls",
|
|
37
|
+
"agent",
|
|
38
|
+
"llm"
|
|
39
|
+
],
|
|
40
|
+
"license": "GPL-3.0-or-later",
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^25.3.0",
|
|
46
|
+
"typescript": "^5.9.3",
|
|
47
|
+
"vitest": "^4.0.18"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"minimatch": "^10.2.2"
|
|
51
|
+
}
|
|
52
|
+
}
|