@limits/openclaw 0.0.1
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 +289 -0
- package/openclaw.plugin.json +42 -0
- package/package.json +31 -0
- package/scripts/configure.js +97 -0
- package/skills/limits-policy-generator/SKILL.md +110 -0
- package/src/config.ts +70 -0
- package/src/configure-wizard.ts +149 -0
- package/src/enforcer.ts +98 -0
- package/src/index.ts +346 -0
- package/src/logger.ts +32 -0
- package/src/token.ts +38 -0
- package/test/integration/mock-saas.test.ts +163 -0
- package/test/unit/failmode.test.ts +114 -0
- package/test/unit/post.test.ts +88 -0
- package/test/unit/pre.test.ts +78 -0
- package/test/unit/token.test.ts +72 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# @limits/openclaw
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@limits/openclaw)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
[](https://nodejs.org/)
|
|
7
|
+
[](https://github.com/limitsdev/limits-openclaw)
|
|
8
|
+
|
|
9
|
+
**Official** OpenClaw plugin for the [Limits](https://limits.dev) platform. Delegates policy enforcement via HTTP. Every decision is made by calling `POST {baseUrl}/openclaw/enforce` before and after each tool call. Optional policy-generator tools let the agent create and update policies from natural language.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Table of Contents
|
|
14
|
+
|
|
15
|
+
- [Installation](#installation)
|
|
16
|
+
- [Quick Start](#quick-start)
|
|
17
|
+
- [How it works](#how-it-works)
|
|
18
|
+
- [Policy-generator tools](#policy-generator-tools)
|
|
19
|
+
- [Configuration](#configuration)
|
|
20
|
+
- [Policy tag convention](#policy-tag-convention-limits-backend)
|
|
21
|
+
- [Enforcement API contract](#limits-api-contract)
|
|
22
|
+
- [CLI reference](#cli-commands)
|
|
23
|
+
- [Security](#security-openclaw--limits)
|
|
24
|
+
- [Development](#development)
|
|
25
|
+
- [Migration from limits-enforcer](#migration-from-limits-enforcer)
|
|
26
|
+
- [License](#license)
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @limits/openclaw
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
yarn add @limits/openclaw
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pnpm add @limits/openclaw
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Then register the plugin with OpenClaw:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
openclaw plugins install ./node_modules/@limits/openclaw
|
|
48
|
+
openclaw plugins enable "@limits/openclaw"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
1. **Install** the plugin (see [Installation](#installation)).
|
|
56
|
+
2. **Configure** your API token (required for enforcement and policy-generator tools):
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
openclaw limits configure
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Or set config manually:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"plugins": {
|
|
67
|
+
"entries": {
|
|
68
|
+
"@limits/openclaw": {
|
|
69
|
+
"enabled": true,
|
|
70
|
+
"config": {
|
|
71
|
+
"apiToken": "sk_your_organization_api_key"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
3. **Allow the plugin** in your agent's tool list so the agent can use policy-generator tools (optional). Edit `~/.openclaw/openclaw.json`:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"agents": {
|
|
84
|
+
"list": [
|
|
85
|
+
{
|
|
86
|
+
"id": "main",
|
|
87
|
+
"tools": { "allow": ["@limits/openclaw"] }
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
4. **Run your agent.** Before and after each tool call, the plugin calls the Limits backend. No code changes required.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## How it works
|
|
99
|
+
|
|
100
|
+
```mermaid
|
|
101
|
+
flowchart LR
|
|
102
|
+
subgraph pre [Before tool call]
|
|
103
|
+
A[Tool invoked] --> B["POST /openclaw/enforce phase=pre"]
|
|
104
|
+
B --> C{Action?}
|
|
105
|
+
C -->|ALLOW| D[Run tool]
|
|
106
|
+
C -->|BLOCK| E[Block call]
|
|
107
|
+
C -->|REWRITE| F[Replace args, then run]
|
|
108
|
+
end
|
|
109
|
+
subgraph post [After tool call]
|
|
110
|
+
D --> G["POST /openclaw/enforce phase=post"]
|
|
111
|
+
G --> H{Action?}
|
|
112
|
+
H -->|ALLOW| I[Return result]
|
|
113
|
+
H -->|BLOCK| J[Safe blocked message]
|
|
114
|
+
H -->|REDACT| K[Redacted result]
|
|
115
|
+
H -->|REWRITE| L[Rewritten result]
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
1. **Before each tool call** (`before_tool_call`): the plugin POSTs `phase: "pre"` with tool name and args. Limits returns `ALLOW`, `BLOCK`, or `REWRITE` (with `rewriteArgs`). The plugin blocks the call, allows it, or replaces the tool arguments accordingly.
|
|
120
|
+
2. **After each tool call** (`after_tool_call`): the plugin POSTs `phase: "post"` with tool name, args, and result. Limits returns `ALLOW`, `BLOCK`, `REDACT`, or `REWRITE` (with `redactedResult` or `rewrittenResult`). The plugin leaves the result unchanged, replaces it with a safe blocked response, or replaces it with the redacted/rewritten value.
|
|
121
|
+
|
|
122
|
+
**Compatibility:** This plugin requires an OpenClaw build where tool hooks are wired into the execution path. If you see **`[@limits/openclaw] before_tool_call observed`** in logs once after a tool runs, hooks are working. See [OpenClaw #6535](https://github.com/openclaw/openclaw/issues/6535) if hooks never fire.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Policy-generator tools
|
|
127
|
+
|
|
128
|
+
When `apiToken` is set, the plugin registers two optional tools and loads the **limits-policy-generator** skill:
|
|
129
|
+
|
|
130
|
+
| Tool | Description |
|
|
131
|
+
|------|-------------|
|
|
132
|
+
| **`limits_generate_create_policy`** | Generate a new policy from natural language and create it on the Limits backend (`POST /api/policies/generatecreate`). |
|
|
133
|
+
| **`limits_generate_update_policy`** | Generate updates from natural language and apply to an existing policy (`POST /api/policies/:id/generateupdate`). |
|
|
134
|
+
|
|
135
|
+
The agent can then fulfill requests like “create a policy that blocks payment tools” or “update my policy to also block stripe_*” by calling these tools.
|
|
136
|
+
|
|
137
|
+
### Add the skill to the workspace
|
|
138
|
+
|
|
139
|
+
When you run `openclaw limits configure` (or `npm run configure`), you can choose to add the limits-policy-generator skill to your OpenClaw workspace when prompted.
|
|
140
|
+
|
|
141
|
+
Optionally, you can copy it manually. The example below is for Unix; on Windows use the wizard or PowerShell equivalents (e.g. `New-Item -ItemType Directory -Force` and `Copy-Item -Recurse`).
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
mkdir -p ~/.openclaw/workspace/skills/limits-policy-generator
|
|
145
|
+
cp -r ./node_modules/@limits/openclaw/skills/limits-policy-generator/. ~/.openclaw/workspace/skills/limits-policy-generator/
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Expose tools to the agent
|
|
149
|
+
|
|
150
|
+
The plugin registers these tools as **optional**. Add the plugin to the agent's tool allowlist (see [Quick Start](#quick-start)) so the agent can call them. Use `"@limits/openclaw"` to allow all tools from this plugin, or allow by name: `["limits_generate_create_policy", "limits_generate_update_policy"]`.
|
|
151
|
+
|
|
152
|
+
### Sandboxed agents
|
|
153
|
+
|
|
154
|
+
If the agent runs in a **sandbox**, add `"@limits/openclaw"` (or the tool names) to `tools.sandbox.tools.allow` as well so the sandboxed agent can call the policy tools. Otherwise the agent can have the skill but cannot invoke the tools from inside the sandbox.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Configuration
|
|
159
|
+
|
|
160
|
+
| Key | Required | Default | Description |
|
|
161
|
+
|-----|----------|---------|-------------|
|
|
162
|
+
| `baseUrl` | — | *Fixed in plugin* | Base URL for `POST /openclaw/enforce` and policy-generator API calls. Optional in config for compatibility. |
|
|
163
|
+
| `apiToken` | No | — | Organization API key for `/openclaw/enforce` and policy-generator tools. Fallback when event/context don't provide a token. Store securely or use `LIMITS_ENFORCER_API_TOKEN` env. |
|
|
164
|
+
| `timeoutMs` | No | `2500` | Request timeout in milliseconds. |
|
|
165
|
+
| `failMode` | No | `"allow"` | When Limits is unreachable or errors: **Pre:** `"allow"` → let the call proceed; `"block"` → block the call. **Post:** `"allow"` → keep original result; `"block"` → replace result with a safe blocked message. |
|
|
166
|
+
| `tokenSource` | No | `"event.metadata.apiToken"` | Dot-separated path to the API token. The plugin uses this first; if missing, it falls back to `apiToken`. |
|
|
167
|
+
| `redactLogs` | No | `true` | Whether to avoid logging sensitive data (token/args are never logged). |
|
|
168
|
+
|
|
169
|
+
### Environment overrides
|
|
170
|
+
|
|
171
|
+
| Variable | Maps to |
|
|
172
|
+
|----------|---------|
|
|
173
|
+
| `LIMITS_ENFORCER_TIMEOUT_MS` | `timeoutMs` |
|
|
174
|
+
| `LIMITS_ENFORCER_FAIL_MODE` | `allow` or `block` |
|
|
175
|
+
| `LIMITS_ENFORCER_TOKEN_SOURCE` | token path |
|
|
176
|
+
| `LIMITS_ENFORCER_REDACT_LOGS` | `true` / `1` for true |
|
|
177
|
+
| `LIMITS_ENFORCER_API_TOKEN` | `apiToken` |
|
|
178
|
+
|
|
179
|
+
### Gateway config example
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
{
|
|
183
|
+
"plugins": {
|
|
184
|
+
"entries": {
|
|
185
|
+
"@limits/openclaw": {
|
|
186
|
+
"enabled": true,
|
|
187
|
+
"config": {
|
|
188
|
+
"apiToken": "sk_your_organization_api_key",
|
|
189
|
+
"timeoutMs": 2500,
|
|
190
|
+
"failMode": "allow",
|
|
191
|
+
"tokenSource": "event.metadata.apiToken"
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Policy tag convention (Limits backend)
|
|
202
|
+
|
|
203
|
+
Policies are scoped to OpenClaw tool calls using **tags** in the Limits dashboard:
|
|
204
|
+
|
|
205
|
+
| Tag | Meaning |
|
|
206
|
+
|-----|---------|
|
|
207
|
+
| `openclaw:phase:pre` | Apply only in the **pre** phase (before the tool runs). |
|
|
208
|
+
| `openclaw:phase:post` | Apply only in the **post** phase (after the tool runs, e.g. guardrails on output). |
|
|
209
|
+
| *(no openclaw:phase tag)* | Apply to **both** phases. |
|
|
210
|
+
| `openclaw:tool:stripe.charge` | Apply only to the tool named `stripe.charge`. |
|
|
211
|
+
| `openclaw:tool:stripe.*` | Apply to all tools whose name **starts with** `stripe.`. |
|
|
212
|
+
| *(no openclaw:tool tag)* | Apply to **all** tools. |
|
|
213
|
+
| `openclaw:scope:all` | Explicit scope: policy applies to all tools (required when `OPENCLAW_REQUIRE_SCOPE_TAGS` is set). |
|
|
214
|
+
| `openclaw:scope:tools` | Explicit scope: policy applies to specific tools (required when scope tags are enforced). |
|
|
215
|
+
|
|
216
|
+
**Example:** To block Stripe tool calls with amount > 200, create a **CONDITIONS** policy like `input.args.amount > 200 → BLOCK`, and tag it with `openclaw:phase:pre` and `openclaw:tool:stripe.*`.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Limits API contract
|
|
221
|
+
|
|
222
|
+
**Endpoint:** `POST {baseUrl}/openclaw/enforce`
|
|
223
|
+
|
|
224
|
+
**Request body (JSON)** — same shape for pre and post:
|
|
225
|
+
|
|
226
|
+
| Field | Pre | Post | Description |
|
|
227
|
+
|-------|-----|------|-------------|
|
|
228
|
+
| `phase` | ✓ | ✓ | `"pre"` or `"post"`. |
|
|
229
|
+
| `apiToken` | ✓ | ✓ | String from `tokenSource` (identifies the org on Limits). |
|
|
230
|
+
| `tool` | ✓ | ✓ | Object: `name`, `args`, optional `toolCallId`. For **post** only, `tool` also includes `result`. |
|
|
231
|
+
| `context` | ✓ | ✓ | Optional: `requestId`, `runId`, `sessionKey`, `agentId`, `channel`, `userMessageSummary`. |
|
|
232
|
+
|
|
233
|
+
**Response (JSON):**
|
|
234
|
+
|
|
235
|
+
- **Pre:** `{ "action": "ALLOW" }` | `{ "action": "BLOCK", "reason": "..." }` | `{ "action": "REWRITE", "rewriteArgs": { ... } }`
|
|
236
|
+
- **Post:** `{ "action": "ALLOW" }` | `{ "action": "BLOCK", "reason": "..." }` | `{ "action": "REDACT", "redactedResult": ... }` | `{ "action": "REWRITE", "rewrittenResult": ... }`
|
|
237
|
+
|
|
238
|
+
**Decision precedence:** Deny wins — any `BLOCK` overrides `ALLOW`. Document your own precedence for REWRITE/REDACT if you combine multiple policies.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## CLI commands
|
|
243
|
+
|
|
244
|
+
| Command | Description |
|
|
245
|
+
|---------|-------------|
|
|
246
|
+
| `openclaw plugins install <path>` | Install plugin from path. |
|
|
247
|
+
| `openclaw plugins install -l <path>` | Link plugin (no copy, for development). |
|
|
248
|
+
| `openclaw plugins enable "@limits/openclaw"` | Enable the plugin. |
|
|
249
|
+
| `openclaw plugins list` | List installed plugins. |
|
|
250
|
+
| `openclaw plugins doctor` | Check plugin health. |
|
|
251
|
+
| `openclaw limits configure` | Interactive wizard to set API token and sandbox allowlist. |
|
|
252
|
+
| `openclaw config get 'plugins.entries["@limits/openclaw"]'` | Show plugin config. |
|
|
253
|
+
| `openclaw config set 'plugins.entries["@limits/openclaw"].config.apiToken' "sk_..."` | Set apiToken manually. |
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Security (OpenClaw ↔ Limits)
|
|
258
|
+
|
|
259
|
+
- Use **HTTPS** in production.
|
|
260
|
+
- For higher assurance (e.g. internal networks), consider **mTLS** or a **shared HMAC** header in addition to `apiToken`.
|
|
261
|
+
- Apply **rate limiting** and auth on the Limits endpoint.
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Development
|
|
266
|
+
|
|
267
|
+
- **Tests:** `npm test` (Vitest). Unit tests mock the enforcer; the integration test runs a real HTTP server and exercises the full pre/post flow.
|
|
268
|
+
- **Retries:** The enforcer client retries up to 2 times on HTTP 429 or 5xx with exponential backoff (200 ms, 400 ms). Non-retryable errors trigger `failMode` immediately.
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Migration from limits-enforcer
|
|
273
|
+
|
|
274
|
+
If you were using the plugin under the old name **limits-enforcer**, update your config and CLI usage:
|
|
275
|
+
|
|
276
|
+
| Before | After |
|
|
277
|
+
|--------|-------|
|
|
278
|
+
| Plugin ID / allowlist: `"limits-enforcer"` | `"@limits/openclaw"` |
|
|
279
|
+
| Config path: `plugins.entries.limits-enforcer` | `plugins.entries["@limits/openclaw"]` |
|
|
280
|
+
| CLI command: `openclaw limits-enforcer configure` | `openclaw limits configure` |
|
|
281
|
+
| Log prefix: `[limits-enforcer]` | `[@limits/openclaw]` |
|
|
282
|
+
|
|
283
|
+
After renaming, reinstall the plugin and enable `"@limits/openclaw"`.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## License
|
|
288
|
+
|
|
289
|
+
MIT — [Limits](https://limits.dev) — [GitHub](https://github.com/limitsdev/limits-openclaw)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "@limits/openclaw",
|
|
3
|
+
"name": "Limits OpenClaw",
|
|
4
|
+
"description": "Delegates policy enforcement to the Limits platform before and after every tool call. Optional policy-generator tools for creating/updating policies from natural language.",
|
|
5
|
+
"version": "0.0.1",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"configSchema": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"additionalProperties": true,
|
|
10
|
+
"properties": {
|
|
11
|
+
"baseUrl": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"description": "Base URL for POST /openclaw/enforce and for policy-generator API calls. Same URL for enforcement and policy tools."
|
|
14
|
+
},
|
|
15
|
+
"apiToken": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"description": "Organization API key for /openclaw/enforce and policy-generator tools. Store securely in gateway config or use LIMITS_ENFORCER_API_TOKEN env."
|
|
18
|
+
},
|
|
19
|
+
"timeoutMs": {
|
|
20
|
+
"type": "number",
|
|
21
|
+
"default": 2500,
|
|
22
|
+
"description": "Request timeout in milliseconds."
|
|
23
|
+
},
|
|
24
|
+
"failMode": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"enum": ["allow", "block"],
|
|
27
|
+
"default": "allow",
|
|
28
|
+
"description": "When Limits backend is unreachable: allow = proceed; block = block the call (pre) or replace result (post)."
|
|
29
|
+
},
|
|
30
|
+
"tokenSource": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"default": "event.metadata.apiToken",
|
|
33
|
+
"description": "Dot-separated path to API token: event.metadata.apiToken, ctx.auth.token, or env.LIMITS_API_TOKEN."
|
|
34
|
+
},
|
|
35
|
+
"redactLogs": {
|
|
36
|
+
"type": "boolean",
|
|
37
|
+
"default": true,
|
|
38
|
+
"description": "Whether to avoid logging sensitive data."
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@limits/openclaw",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Delegates policy enforcement to the Limits platform before and after every tool call. Optional policy-generator tools for creating/updating policies from natural language.",
|
|
5
|
+
"keywords": ["openclaw", "limits", "policy", "enforcement", "agent", "guardrails", "ai-safety"],
|
|
6
|
+
"author": "Limits",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": { "type": "git", "url": "git+https://github.com/limitsdev/limits-openclaw.git" },
|
|
9
|
+
"bugs": { "url": "https://github.com/limitsdev/limits-openclaw/issues" },
|
|
10
|
+
"homepage": "https://github.com/limitsdev/limits-openclaw#readme",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./src/index.ts",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:watch": "vitest",
|
|
16
|
+
"configure": "node scripts/configure.js"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18.0.0"
|
|
20
|
+
},
|
|
21
|
+
"openclaw": {
|
|
22
|
+
"hooks": ["./"],
|
|
23
|
+
"extensions": ["./"],
|
|
24
|
+
"events": ["before_tool_call", "after_tool_call"]
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"typescript": "^5.0.0",
|
|
28
|
+
"@types/node": "^20.0.0",
|
|
29
|
+
"vitest": "^2.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Interactive configure for @limits/openclaw plugin.
|
|
4
|
+
* Writes to OpenClaw config via: openclaw config set plugins.entries["@limits/openclaw"].config.<key> <value>
|
|
5
|
+
*
|
|
6
|
+
* Run from plugin root: node scripts/configure.js
|
|
7
|
+
* Or: npm run configure
|
|
8
|
+
*
|
|
9
|
+
* Prerequisite: openclaw plugins install <path> and openclaw plugins enable "@limits/openclaw"
|
|
10
|
+
* (so plugins.entries["@limits/openclaw"] exists).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createInterface } from "node:readline";
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import os from "node:os";
|
|
19
|
+
|
|
20
|
+
const PREFIX = 'plugins.entries["@limits/openclaw"].config';
|
|
21
|
+
const SKILL_NAME = "limits-policy-generator";
|
|
22
|
+
|
|
23
|
+
function getPluginRoot() {
|
|
24
|
+
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
return join(scriptDir, "..");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getWorkspaceSkillsDest() {
|
|
29
|
+
const workspaceRoot =
|
|
30
|
+
process.env.OPENCLAW_WORKSPACE || join(os.homedir(), ".openclaw", "workspace");
|
|
31
|
+
return join(workspaceRoot, "skills", SKILL_NAME);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function copySkillToWorkspace() {
|
|
35
|
+
const pluginRoot = getPluginRoot();
|
|
36
|
+
const source = join(pluginRoot, "skills", SKILL_NAME);
|
|
37
|
+
const dest = getWorkspaceSkillsDest();
|
|
38
|
+
|
|
39
|
+
if (!fs.existsSync(source)) {
|
|
40
|
+
console.log("\nSkill source not found, skipping.");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
45
|
+
fs.cpSync(source, dest, { recursive: true });
|
|
46
|
+
console.log(`\nCopied ${SKILL_NAME} to ${dest}.`);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error("\nFailed to copy skill:", err instanceof Error ? err.message : String(err));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function ask(rl, question, defaultValue = "") {
|
|
53
|
+
const prompt = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `;
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
rl.question(prompt, (answer) => {
|
|
56
|
+
resolve(typeof answer === "string" && answer.trim() !== "" ? answer.trim() : defaultValue);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function runOpenClawConfigSet(key, value) {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const fullKey = `${PREFIX}.${key}`;
|
|
64
|
+
const child = spawn("openclaw", ["config", "set", fullKey, value], {
|
|
65
|
+
stdio: "inherit",
|
|
66
|
+
shell: true,
|
|
67
|
+
});
|
|
68
|
+
child.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`openclaw config set exited ${code}`))));
|
|
69
|
+
child.on("error", reject);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function main() {
|
|
74
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
75
|
+
|
|
76
|
+
console.log("\nConfigure @limits/openclaw plugin (writes to OpenClaw config via openclaw config set).");
|
|
77
|
+
console.log("Prerequisite: openclaw plugins install <path> (or -l to link).");
|
|
78
|
+
console.log("Tip: After linking, you can also run: openclaw limits configure\n");
|
|
79
|
+
|
|
80
|
+
const apiToken = await ask(rl, "Organization API key (apiToken) — required for enforce and policy-generator tools", process.env.LIMITS_ENFORCER_API_TOKEN || "");
|
|
81
|
+
const addSkillAnswer = await ask(rl, "Add limits-policy-generator skill to OpenClaw workspace?", "Y");
|
|
82
|
+
|
|
83
|
+
rl.close();
|
|
84
|
+
|
|
85
|
+
if (apiToken) await runOpenClawConfigSet("apiToken", JSON.stringify(apiToken));
|
|
86
|
+
|
|
87
|
+
const addSkillYes = /^y(es)?$/i.test(addSkillAnswer.trim());
|
|
88
|
+
if (addSkillYes) copySkillToWorkspace();
|
|
89
|
+
|
|
90
|
+
console.log("\nDone. Restart the gateway if it is running.");
|
|
91
|
+
console.log('Verify: openclaw config get plugins.entries["@limits/openclaw"]');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
main().catch((err) => {
|
|
95
|
+
console.error(err);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: limits-policy-generator
|
|
3
|
+
description: "Generate and create or update policy rules on the Limits SaaS from natural language. Use when the user asks to add, change, or create policy rules enforced by the Limits backend."
|
|
4
|
+
metadata: {"openclaw": {"emoji": "📜", "requires": {"config": ["plugins.entries[\"@limits/openclaw\"].config.apiToken"]}}}
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Limits Policy Generator
|
|
8
|
+
|
|
9
|
+
You have two tools for creating and updating policies on the Limits SaaS from natural language. They call the Limits backend so the user's enforcement rules are created or updated there (and apply to tool-call enforcement via the @limits/openclaw plugin).
|
|
10
|
+
|
|
11
|
+
## When to use this skill
|
|
12
|
+
|
|
13
|
+
Use these tools when the user says things like:
|
|
14
|
+
|
|
15
|
+
- "Create a policy that blocks all payment tools"
|
|
16
|
+
- "Add a rule: never run bash with rm -rf"
|
|
17
|
+
- "Generate a policy from this: block transactions over 500"
|
|
18
|
+
- "Update my policy to also block stripe_* tools"
|
|
19
|
+
- "I want to add a guardrail that redacts emails from tool output"
|
|
20
|
+
- "Change the payment limit to 700"
|
|
21
|
+
|
|
22
|
+
## Tools available
|
|
23
|
+
|
|
24
|
+
### `limits_generate_create_policy`
|
|
25
|
+
|
|
26
|
+
Generate a new policy from natural language and create it on the Limits backend. Use when the user wants to **add** a new policy.
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
limits_generate_create_policy(
|
|
30
|
+
input="Block any tool whose name starts with stripe_ or payment_. Allow all other tools.",
|
|
31
|
+
mode="INSTRUCTIONS",
|
|
32
|
+
tools=["stripe_.*", "payment_.*"]
|
|
33
|
+
)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- **input** (required): Natural-language description of the policy (what to block, allow, or require).
|
|
37
|
+
- **mode** (optional): `"INSTRUCTIONS"` | `"CONDITIONS"` | `"GUARDRAIL"`. Default is INSTRUCTIONS. Use GUARDRAIL for rules that scan tool **output** (e.g. redact PII).
|
|
38
|
+
- **tools** (required): Which tool calls this policy applies to. See "Where to apply" section below.
|
|
39
|
+
|
|
40
|
+
### `limits_generate_update_policy`
|
|
41
|
+
|
|
42
|
+
Generate updates from natural language and apply them to an **existing** policy. Use when the user wants to **change** a policy they already have.
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
limits_generate_update_policy(
|
|
46
|
+
policyId="uuid-of-existing-policy",
|
|
47
|
+
input="Also block transfer_money. Keep the existing amount limit.",
|
|
48
|
+
mode="INSTRUCTIONS"
|
|
49
|
+
)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- **policyId** (required): The ID of the existing policy to update (UUID).
|
|
53
|
+
- **input** (required): Natural-language description of the changes or additions.
|
|
54
|
+
- **mode** (optional): Same as above.
|
|
55
|
+
- **Note:** Update does **not** change which tools the policy applies to — scope is fixed at creation time. If the user wants to change scope, they should create a new policy.
|
|
56
|
+
|
|
57
|
+
## Where to apply (tools parameter)
|
|
58
|
+
|
|
59
|
+
The `tools` parameter on `limits_generate_create_policy` is **required** and controls which tool calls the policy is enforced on.
|
|
60
|
+
|
|
61
|
+
### Values
|
|
62
|
+
|
|
63
|
+
| Value | Meaning |
|
|
64
|
+
|-------|---------|
|
|
65
|
+
| `["*"]` | Apply to **all** tool calls / all requests |
|
|
66
|
+
| `["<tool_name>"]` | Apply to the exact tool (use only names from the agent's actual tool list) |
|
|
67
|
+
| `["<prefix>.*"]` | Apply to all tools whose name starts with that prefix (e.g. `payment_.*`) |
|
|
68
|
+
|
|
69
|
+
Use `prefix.*` (dot-star) for prefix matching. The backend also accepts `prefix_*` and normalizes it to `prefix_.*`. **Use only tool names or prefixes that exist in the user's / agent's available tools** — do not invent names.
|
|
70
|
+
|
|
71
|
+
### Ask-once logic (required)
|
|
72
|
+
|
|
73
|
+
**Do not call `limits_generate_create_policy` until you know scope.** If the user did not say scope, ask once.
|
|
74
|
+
|
|
75
|
+
- User says **"everywhere"**, **"all requests"**, **"globally"**, or **"all tools"** → use `["*"]` without asking.
|
|
76
|
+
- User **explicitly names specific tools** (e.g. "for read_file", "payment tools", "stripe_*") → use those tools without asking.
|
|
77
|
+
- User **does not say** "all" / "everywhere" / "globally" and **does not name any tools** (e.g. "create a policy if currency is JOD allow request", "create a policy that blocks payments") → you **must ask once** before calling the tool: _"Which tools should this policy apply to: all tool calls, or only specific tools? If specific, which tool names from your available tools?"_ Then call the tool with the user's answer.
|
|
78
|
+
|
|
79
|
+
Do not assume scope. Do not call the create-policy tool immediately when the user only describes the rule (e.g. "if currency is JOD allow") without stating where it applies. Ask once, then call.
|
|
80
|
+
|
|
81
|
+
### Tool names: use source of truth only
|
|
82
|
+
|
|
83
|
+
When setting the `tools` parameter, use **only tool names from the agent's actual available tools** (the list of tools you have access to). Do not invent or assume tool names. If the user wants "specific tools", list the relevant tools from your real tool list and use those exact names (or prefix patterns like `name_.*` that match them). If you do not have a list of available tools, ask the user which tools should be in scope and use only names they confirm.
|
|
84
|
+
|
|
85
|
+
## Recommended flow
|
|
86
|
+
|
|
87
|
+
1. If the user wants a **new** policy:
|
|
88
|
+
- **First determine scope.** If the user did not say "all tools" / "everywhere" / "globally" and did not name specific tools, ask once: "Which tools should this apply to: all tool calls or specific tools? If specific, which ones?"
|
|
89
|
+
- Only then call `limits_generate_create_policy` with `input`, `tools` (from user or from their actual tool list), and optionally `mode`.
|
|
90
|
+
- Confirm what was created and **where it applies** (e.g. "This policy applies to all tools" or "This policy applies to [tool names]").
|
|
91
|
+
2. If the user wants to **change** an existing policy:
|
|
92
|
+
- Use `limits_generate_update_policy` with the policy id and the change description. (You may need to list or find the policy id first if the user says "update my payment policy".)
|
|
93
|
+
- Remind the user that **scope (which tools the policy applies to) cannot be changed via update** — it stays as set when the policy was created.
|
|
94
|
+
3. Policies on the Limits backend apply to tool-call enforcement (via @limits/openclaw) for that organization.
|
|
95
|
+
|
|
96
|
+
## Sandbox vs host
|
|
97
|
+
|
|
98
|
+
**If you are running inside a sandbox:** The `limits_generate_create_policy` and `limits_generate_update_policy` tools are only available if the gateway config allows them in the sandbox (e.g. `tools.sandbox.tools.allow` includes `"@limits/openclaw"`). If those tools are not in your tool list, you cannot invoke them from this environment.
|
|
99
|
+
|
|
100
|
+
- **To run the policy tools from the sandbox:** The admin must add `@limits/openclaw` to the agent allowlist and to `tools.sandbox.tools.allow` (see the @limits/openclaw README). After that, you can call the tools from here.
|
|
101
|
+
- **If the tools are still not available:** Tell the user to run the policy command on the **host** or an agent instance that has the Limits backend wired up and the tools allowed.
|
|
102
|
+
|
|
103
|
+
## Configuration
|
|
104
|
+
|
|
105
|
+
The plugin must be configured with:
|
|
106
|
+
|
|
107
|
+
- **baseUrl**: Base URL of the Limits API (e.g. `https://api.limits.dev`). Used for enforce and policy-generator tools.
|
|
108
|
+
- **apiToken**: Organization API key (Bearer) for the Limits backend. Used for enforce and policy-generator tools.
|
|
109
|
+
|
|
110
|
+
Without these, the tools will not be available or will return an error asking the user to configure them.
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin config: loaded from gateway config with env var overrides.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const STATIC_BASE_URL = "https://extensionally-jettisonable-rosann.ngrok-free.dev";
|
|
6
|
+
|
|
7
|
+
export interface EnforcerConfig {
|
|
8
|
+
baseUrl: string;
|
|
9
|
+
timeoutMs: number;
|
|
10
|
+
failMode: "allow" | "block";
|
|
11
|
+
tokenSource: string;
|
|
12
|
+
redactLogs: boolean;
|
|
13
|
+
/** Optional: organization API key sent as apiToken to POST /openclaw/enforce and for policy-generator tools. Used when event/context don't provide a token (fallback for all requests). */
|
|
14
|
+
apiToken?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DEFAULTS: EnforcerConfig = {
|
|
18
|
+
baseUrl: STATIC_BASE_URL,
|
|
19
|
+
timeoutMs: 2500,
|
|
20
|
+
failMode: "allow",
|
|
21
|
+
tokenSource: "event.metadata.apiToken",
|
|
22
|
+
redactLogs: true,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type ApiLike = {
|
|
26
|
+
config?: {
|
|
27
|
+
plugins?: {
|
|
28
|
+
entries?: Record<
|
|
29
|
+
string,
|
|
30
|
+
{ enabled?: boolean; config?: Partial<EnforcerConfig> }
|
|
31
|
+
>;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function loadConfig(api: ApiLike): EnforcerConfig {
|
|
37
|
+
const raw =
|
|
38
|
+
api.config?.plugins?.entries?.["@limits/openclaw"]?.config ?? ({} as Partial<EnforcerConfig>);
|
|
39
|
+
|
|
40
|
+
// baseUrl defaults to static; not in wizard or schema so users don't edit it (config override only for tests/advanced)
|
|
41
|
+
const baseUrl = raw.baseUrl ?? DEFAULTS.baseUrl;
|
|
42
|
+
const timeoutMs =
|
|
43
|
+
typeof process.env.LIMITS_ENFORCER_TIMEOUT_MS === "string"
|
|
44
|
+
? parseInt(process.env.LIMITS_ENFORCER_TIMEOUT_MS, 10)
|
|
45
|
+
: raw.timeoutMs ?? DEFAULTS.timeoutMs;
|
|
46
|
+
const failMode =
|
|
47
|
+
(process.env.LIMITS_ENFORCER_FAIL_MODE as "allow" | "block") ??
|
|
48
|
+
raw.failMode ??
|
|
49
|
+
DEFAULTS.failMode;
|
|
50
|
+
const tokenSource =
|
|
51
|
+
process.env.LIMITS_ENFORCER_TOKEN_SOURCE ??
|
|
52
|
+
raw.tokenSource ??
|
|
53
|
+
DEFAULTS.tokenSource;
|
|
54
|
+
const redactLogs =
|
|
55
|
+
process.env.LIMITS_ENFORCER_REDACT_LOGS !== undefined
|
|
56
|
+
? process.env.LIMITS_ENFORCER_REDACT_LOGS === "true" ||
|
|
57
|
+
process.env.LIMITS_ENFORCER_REDACT_LOGS === "1"
|
|
58
|
+
: raw.redactLogs ?? DEFAULTS.redactLogs;
|
|
59
|
+
const apiToken =
|
|
60
|
+
process.env.LIMITS_ENFORCER_API_TOKEN ?? raw.apiToken ?? undefined;
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
baseUrl,
|
|
64
|
+
timeoutMs: Number.isNaN(timeoutMs) ? DEFAULTS.timeoutMs : timeoutMs,
|
|
65
|
+
failMode: failMode === "block" ? "block" : "allow",
|
|
66
|
+
tokenSource: typeof tokenSource === "string" ? tokenSource : DEFAULTS.tokenSource,
|
|
67
|
+
redactLogs: Boolean(redactLogs),
|
|
68
|
+
...(typeof apiToken === "string" && apiToken.length > 0 && { apiToken }),
|
|
69
|
+
};
|
|
70
|
+
}
|