limits-openclaw 0.0.6

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 ADDED
@@ -0,0 +1,325 @@
1
+ # limits-openclaw
2
+
3
+ [![npm version](https://img.shields.io/npm/v/limits-openclaw.svg?style=flat-square)](https://www.npmjs.com/package/limits-openclaw)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://opensource.org/licenses/MIT)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178C6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
6
+ [![Node.js](https://img.shields.io/badge/Node.js-18%2B-339933?style=flat-square&logo=node.js&logoColor=white)](https://nodejs.org/)
7
+ [![GitHub](https://img.shields.io/badge/GitHub-limitsdev%2Flimits--openclaw-181717?style=flat-square&logo=github)](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
+ - [Troubleshooting](#troubleshooting)
27
+ - [License](#license)
28
+
29
+ ---
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ npm install limits-openclaw
35
+ ```
36
+
37
+ ```bash
38
+ yarn add limits-openclaw
39
+ ```
40
+
41
+ ```bash
42
+ pnpm add limits-openclaw
43
+ ```
44
+
45
+ Then register the plugin with OpenClaw:
46
+
47
+ ```bash
48
+ openclaw plugins install ./node_modules/limits-openclaw
49
+ openclaw plugins enable "limits-openclaw"
50
+ openclaw gateway restart
51
+ ```
52
+
53
+ ---
54
+
55
+ ## Quick Start
56
+
57
+ 1. **Install** the plugin (see [Installation](#installation)).
58
+ 2. **Configure** your API token (required for enforcement and policy-generator tools):
59
+
60
+ ```bash
61
+ openclaw limits configure
62
+ ```
63
+
64
+ If you see **`error: unknown command 'limits'`**, the OpenClaw CLI may not load plugin commands in your environment. Run the configure script directly (from a directory where the package is installed):
65
+
66
+ ```bash
67
+ node node_modules/limits-openclaw/scripts/configure.js
68
+ ```
69
+
70
+ Or set config manually:
71
+
72
+ ```json
73
+ {
74
+ "plugins": {
75
+ "entries": {
76
+ "limits-openclaw": {
77
+ "enabled": true,
78
+ "config": {
79
+ "apiToken": "sk_your_organization_api_key"
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
85
+ ```
86
+
87
+ 3. **Allow the plugin** in your agent's tool list so the agent can use policy-generator tools (optional). Edit `~/.openclaw/openclaw.json`:
88
+
89
+ ```json
90
+ {
91
+ "agents": {
92
+ "list": [
93
+ {
94
+ "id": "main",
95
+ "tools": { "allow": ["limits-openclaw"] }
96
+ }
97
+ ]
98
+ }
99
+ }
100
+ ```
101
+
102
+ 4. **Run your agent.** Before and after each tool call, the plugin calls the Limits backend. No code changes required.
103
+
104
+ ---
105
+
106
+ ## How it works
107
+
108
+ ```mermaid
109
+ flowchart LR
110
+ subgraph pre [Before tool call]
111
+ A[Tool invoked] --> B["POST /openclaw/enforce phase=pre"]
112
+ B --> C{Action?}
113
+ C -->|ALLOW| D[Run tool]
114
+ C -->|BLOCK| E[Block call]
115
+ C -->|REWRITE| F[Replace args, then run]
116
+ end
117
+ subgraph post [After tool call]
118
+ D --> G["POST /openclaw/enforce phase=post"]
119
+ G --> H{Action?}
120
+ H -->|ALLOW| I[Return result]
121
+ H -->|BLOCK| J[Safe blocked message]
122
+ H -->|REDACT| K[Redacted result]
123
+ H -->|REWRITE| L[Rewritten result]
124
+ end
125
+ ```
126
+
127
+ 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.
128
+ 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.
129
+
130
+ **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.
131
+
132
+ ---
133
+
134
+ ## Policy-generator tools
135
+
136
+ When `apiToken` is set, the plugin registers two optional tools and loads the **limits-policy-generator** skill:
137
+
138
+ | Tool | Description |
139
+ |------|-------------|
140
+ | **`limits_generate_create_policy`** | Generate a new policy from natural language and create it on the Limits backend (`POST /api/policies/generatecreate`). |
141
+ | **`limits_generate_update_policy`** | Generate updates from natural language and apply to an existing policy (`POST /api/policies/:id/generateupdate`). |
142
+
143
+ 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.
144
+
145
+ ### Add the skill to the workspace
146
+
147
+ 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.
148
+
149
+ 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`).
150
+
151
+ ```bash
152
+ mkdir -p ~/.openclaw/workspace/skills/limits-policy-generator
153
+ cp -r ./node_modules/limits-openclaw/skills/limits-policy-generator/. ~/.openclaw/workspace/skills/limits-policy-generator/
154
+ ```
155
+
156
+ ### Expose tools to the agent
157
+
158
+ 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"]`.
159
+
160
+ ### Sandboxed agents
161
+
162
+ 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.
163
+
164
+ ---
165
+
166
+ ## Configuration
167
+
168
+ | Key | Required | Default | Description |
169
+ |-----|----------|---------|-------------|
170
+ | `baseUrl` | — | *Fixed in plugin* | Base URL for `POST /openclaw/enforce` and policy-generator API calls. Optional in config for compatibility. |
171
+ | `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. |
172
+ | `timeoutMs` | No | `2500` | Request timeout in milliseconds. |
173
+ | `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. |
174
+ | `tokenSource` | No | `"event.metadata.apiToken"` | Dot-separated path to the API token. The plugin uses this first; if missing, it falls back to `apiToken`. |
175
+ | `redactLogs` | No | `true` | Whether to avoid logging sensitive data (token/args are never logged). |
176
+
177
+ ### Environment overrides
178
+
179
+ | Variable | Maps to |
180
+ |----------|---------|
181
+ | `LIMITS_ENFORCER_TIMEOUT_MS` | `timeoutMs` |
182
+ | `LIMITS_ENFORCER_FAIL_MODE` | `allow` or `block` |
183
+ | `LIMITS_ENFORCER_TOKEN_SOURCE` | token path |
184
+ | `LIMITS_ENFORCER_REDACT_LOGS` | `true` / `1` for true |
185
+ | `LIMITS_ENFORCER_API_TOKEN` | `apiToken` |
186
+
187
+ ### Gateway config example
188
+
189
+ ```json
190
+ {
191
+ "plugins": {
192
+ "entries": {
193
+ "limits-openclaw": {
194
+ "enabled": true,
195
+ "config": {
196
+ "apiToken": "sk_your_organization_api_key",
197
+ "timeoutMs": 2500,
198
+ "failMode": "allow",
199
+ "tokenSource": "event.metadata.apiToken"
200
+ }
201
+ }
202
+ }
203
+ }
204
+ }
205
+ ```
206
+
207
+ ---
208
+
209
+ ## Policy tag convention (Limits backend)
210
+
211
+ Policies are scoped to OpenClaw tool calls using **tags** in the Limits dashboard:
212
+
213
+ | Tag | Meaning |
214
+ |-----|---------|
215
+ | `openclaw:phase:pre` | Apply only in the **pre** phase (before the tool runs). |
216
+ | `openclaw:phase:post` | Apply only in the **post** phase (after the tool runs, e.g. guardrails on output). |
217
+ | *(no openclaw:phase tag)* | Apply to **both** phases. |
218
+ | `openclaw:tool:stripe.charge` | Apply only to the tool named `stripe.charge`. |
219
+ | `openclaw:tool:stripe.*` | Apply to all tools whose name **starts with** `stripe.`. |
220
+ | *(no openclaw:tool tag)* | Apply to **all** tools. |
221
+ | `openclaw:scope:all` | Explicit scope: policy applies to all tools (required when `OPENCLAW_REQUIRE_SCOPE_TAGS` is set). |
222
+ | `openclaw:scope:tools` | Explicit scope: policy applies to specific tools (required when scope tags are enforced). |
223
+
224
+ **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.*`.
225
+
226
+ ---
227
+
228
+ ## Limits API contract
229
+
230
+ **Endpoint:** `POST {baseUrl}/openclaw/enforce`
231
+
232
+ **Request body (JSON)** — same shape for pre and post:
233
+
234
+ | Field | Pre | Post | Description |
235
+ |-------|-----|------|-------------|
236
+ | `phase` | āœ“ | āœ“ | `"pre"` or `"post"`. |
237
+ | `apiToken` | āœ“ | āœ“ | String from `tokenSource` (identifies the org on Limits). |
238
+ | `tool` | āœ“ | āœ“ | Object: `name`, `args`, optional `toolCallId`. For **post** only, `tool` also includes `result`. |
239
+ | `context` | āœ“ | āœ“ | Optional: `requestId`, `runId`, `sessionKey`, `agentId`, `channel`, `userMessageSummary`. |
240
+
241
+ **Response (JSON):**
242
+
243
+ - **Pre:** `{ "action": "ALLOW" }` | `{ "action": "BLOCK", "reason": "..." }` | `{ "action": "REWRITE", "rewriteArgs": { ... } }`
244
+ - **Post:** `{ "action": "ALLOW" }` | `{ "action": "BLOCK", "reason": "..." }` | `{ "action": "REDACT", "redactedResult": ... }` | `{ "action": "REWRITE", "rewrittenResult": ... }`
245
+
246
+ **Decision precedence:** Deny wins — any `BLOCK` overrides `ALLOW`. Document your own precedence for REWRITE/REDACT if you combine multiple policies.
247
+
248
+ ---
249
+
250
+ ## CLI commands
251
+
252
+ | Command | Description |
253
+ |---------|-------------|
254
+ | `openclaw plugins install <path>` | Install plugin from path. |
255
+ | `openclaw plugins install -l <path>` | Link plugin (no copy, for development). |
256
+ | `openclaw plugins enable "limits-openclaw"` | Enable the plugin. |
257
+ | `openclaw plugins list` | List installed plugins. |
258
+ | `openclaw plugins doctor` | Check plugin health. |
259
+ | `openclaw limits configure` | Interactive wizard to set API token and sandbox allowlist. |
260
+ | `openclaw config get 'plugins.entries["limits-openclaw"]'` | Show plugin config. |
261
+ | `openclaw config set 'plugins.entries["limits-openclaw"].config.apiToken' "sk_..."` | Set apiToken manually. |
262
+
263
+ ---
264
+
265
+ ## Security (OpenClaw ↔ Limits)
266
+
267
+ - Use **HTTPS** in production.
268
+ - For higher assurance (e.g. internal networks), consider **mTLS** or a **shared HMAC** header in addition to `apiToken`.
269
+ - Apply **rate limiting** and auth on the Limits endpoint.
270
+
271
+ ---
272
+
273
+ ## Development
274
+
275
+ - **Tests:** `npm test` (Vitest). Unit tests mock the enforcer; the integration test runs a real HTTP server and exercises the full pre/post flow.
276
+ - **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.
277
+
278
+ ---
279
+
280
+ ## Migration from limits-enforcer
281
+
282
+ If you were using the plugin under the old name **limits-enforcer**, update your config and CLI usage:
283
+
284
+ | Before | After |
285
+ |--------|-------|
286
+ | Plugin ID / allowlist: `"limits-enforcer"` | `"limits-openclaw"` |
287
+ | Config path: `plugins.entries.limits-enforcer` | `plugins.entries["limits-openclaw"]` |
288
+ | CLI command: `openclaw limits-enforcer configure` | `openclaw limits configure` |
289
+ | Log prefix: `[limits-enforcer]` | `[limits-openclaw]` |
290
+
291
+ After renaming, reinstall the plugin and enable `"limits-openclaw"`.
292
+
293
+ ---
294
+
295
+ ## Troubleshooting
296
+
297
+ - **Config warning "plugin id mismatch (manifest uses 'limits-openclaw', entry hints 'openclaw')"**
298
+ Some OpenClaw gateways install scoped packages under `extensions/openclaw` instead of `extensions/limits-openclaw`. The gateway then infers the plugin id from the folder name (`openclaw`), which does not match the manifest id `limits-openclaw`, so the plugin may not load and `openclaw limits configure` shows **unknown command 'limits'**.
299
+
300
+ **Workaround — install under the scoped path so the gateway matches the config entry:**
301
+ ```bash
302
+ # Create the scope directory and install the package there (adjust if you use npm -g or another path)
303
+ mkdir -p ~/.openclaw/extensions/@limits
304
+ cp -r ./node_modules/limits-openclaw ~/.openclaw/extensions/@limits/
305
+
306
+ # Tell OpenClaw to load the plugin from that path
307
+ openclaw plugins install ~/.openclaw/extensions/limits-openclaw
308
+ openclaw plugins enable "limits-openclaw"
309
+ openclaw gateway restart
310
+ ```
311
+ Then run `openclaw limits configure`.
312
+
313
+ - **`error: unknown command 'limits'`**
314
+ The plugin did not load (often due to the id mismatch above). Use the workaround above, or run the configure wizard without the OpenClaw CLI:
315
+ ```bash
316
+ node node_modules/limits-openclaw/scripts/configure.js
317
+ ```
318
+ Or from the plugin repo: `npm run configure`.
319
+
320
+
321
+ ---
322
+
323
+ ## License
324
+
325
+ MIT — [Limits](https://limits.dev) — [GitHub](https://github.com/limitsdev/limits-openclaw)
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Plugin config: loaded from gateway config with env var overrides.
3
+ */
4
+ const STATIC_BASE_URL = "https://extensionally-jettisonable-rosann.ngrok-free.dev";
5
+ const DEFAULTS = {
6
+ baseUrl: STATIC_BASE_URL,
7
+ timeoutMs: 2500,
8
+ failMode: "allow",
9
+ tokenSource: "event.metadata.apiToken",
10
+ redactLogs: true,
11
+ };
12
+ export function loadConfig(api) {
13
+ const raw = api.config?.plugins?.entries?.["limits-openclaw"]?.config ?? {};
14
+ // baseUrl defaults to static; not in wizard or schema so users don't edit it (config override only for tests/advanced)
15
+ const baseUrl = raw.baseUrl ?? DEFAULTS.baseUrl;
16
+ const timeoutMs = typeof process.env.LIMITS_ENFORCER_TIMEOUT_MS === "string"
17
+ ? parseInt(process.env.LIMITS_ENFORCER_TIMEOUT_MS, 10)
18
+ : raw.timeoutMs ?? DEFAULTS.timeoutMs;
19
+ const failMode = process.env.LIMITS_ENFORCER_FAIL_MODE ??
20
+ raw.failMode ??
21
+ DEFAULTS.failMode;
22
+ const tokenSource = process.env.LIMITS_ENFORCER_TOKEN_SOURCE ??
23
+ raw.tokenSource ??
24
+ DEFAULTS.tokenSource;
25
+ const redactLogs = process.env.LIMITS_ENFORCER_REDACT_LOGS !== undefined
26
+ ? process.env.LIMITS_ENFORCER_REDACT_LOGS === "true" ||
27
+ process.env.LIMITS_ENFORCER_REDACT_LOGS === "1"
28
+ : raw.redactLogs ?? DEFAULTS.redactLogs;
29
+ const apiToken = process.env.LIMITS_ENFORCER_API_TOKEN ?? raw.apiToken ?? undefined;
30
+ return {
31
+ baseUrl,
32
+ timeoutMs: Number.isNaN(timeoutMs) ? DEFAULTS.timeoutMs : timeoutMs,
33
+ failMode: failMode === "block" ? "block" : "allow",
34
+ tokenSource: typeof tokenSource === "string" ? tokenSource : DEFAULTS.tokenSource,
35
+ redactLogs: Boolean(redactLogs),
36
+ ...(typeof apiToken === "string" && apiToken.length > 0 && { apiToken }),
37
+ };
38
+ }
@@ -0,0 +1,117 @@
1
+ import { createInterface } from "node:readline";
2
+ import { spawn } from "node:child_process";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import fs from "node:fs";
6
+ import os from "node:os";
7
+ const CONFIG_PREFIX = 'plugins.entries["limits-openclaw"].config';
8
+ const SKILL_NAME = "limits-policy-generator";
9
+ function getPluginRoot() {
10
+ const currentDir = dirname(fileURLToPath(import.meta.url));
11
+ return join(currentDir, "..");
12
+ }
13
+ function getWorkspaceSkillsDest() {
14
+ const workspaceRoot = process.env.OPENCLAW_WORKSPACE || join(os.homedir(), ".openclaw", "workspace");
15
+ return join(workspaceRoot, "skills", SKILL_NAME);
16
+ }
17
+ function copySkillToWorkspace() {
18
+ const pluginRoot = getPluginRoot();
19
+ const source = join(pluginRoot, "skills", SKILL_NAME);
20
+ const dest = getWorkspaceSkillsDest();
21
+ if (!fs.existsSync(source)) {
22
+ console.log("\nSkill source not found, skipping.");
23
+ return;
24
+ }
25
+ try {
26
+ fs.mkdirSync(dest, { recursive: true });
27
+ fs.cpSync(source, dest, { recursive: true });
28
+ console.log(`\nCopied ${SKILL_NAME} to ${dest}.`);
29
+ }
30
+ catch (err) {
31
+ console.error("\nFailed to copy skill:", err instanceof Error ? err.message : String(err));
32
+ }
33
+ }
34
+ export function ask(rl, question, defaultValue = "") {
35
+ const prompt = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `;
36
+ return new Promise((resolve) => {
37
+ rl.question(prompt, (answer) => {
38
+ resolve(typeof answer === "string" && answer.trim() !== "" ? answer.trim() : defaultValue);
39
+ });
40
+ });
41
+ }
42
+ function runConfigSet(key, value) {
43
+ return new Promise((resolve, reject) => {
44
+ const fullKey = `${CONFIG_PREFIX}.${key}`;
45
+ const child = spawn("openclaw", ["config", "set", fullKey, value], {
46
+ stdio: "inherit",
47
+ shell: true,
48
+ });
49
+ child.on("close", (code) => code === 0 ? resolve() : reject(new Error(`openclaw config set exited ${code}`)));
50
+ child.on("error", reject);
51
+ });
52
+ }
53
+ function runConfigGet(fullKey) {
54
+ return new Promise((resolve) => {
55
+ const child = spawn("openclaw", ["config", "get", fullKey], {
56
+ stdio: ["inherit", "pipe", "inherit"],
57
+ shell: true,
58
+ });
59
+ let out = "";
60
+ child.stdout?.on("data", (d) => (out += d.toString()));
61
+ child.on("close", () => resolve(out.trim()));
62
+ child.on("error", () => resolve(""));
63
+ });
64
+ }
65
+ function runConfigSetFull(fullKey, value) {
66
+ return new Promise((resolve, reject) => {
67
+ const child = spawn("openclaw", ["config", "set", fullKey, value], {
68
+ stdio: "inherit",
69
+ shell: true,
70
+ });
71
+ child.on("close", (code) => code === 0 ? resolve() : reject(new Error(`openclaw config set exited ${code}`)));
72
+ child.on("error", reject);
73
+ });
74
+ }
75
+ /**
76
+ * Run the configure wizard (interactive prompts, then openclaw config set).
77
+ * Call this when the user runs `openclaw limits configure` or after link.
78
+ */
79
+ export async function runConfigureWizard() {
80
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
81
+ console.log("\nšŸ¦ž Limits OpenClaw — configure plugin (API token)");
82
+ console.log(" Base URL is fixed. apiToken is used for /openclaw/enforce and policy-generator tools. Run after: openclaw plugins install -l <path>\n");
83
+ const apiToken = await ask(rl, "Organization API key (apiToken) — required for enforce and policy-generator tools", process.env.LIMITS_ENFORCER_API_TOKEN ?? "");
84
+ const sandboxAnswer = await ask(rl, "Do you run agents inside a sandbox?", "N");
85
+ const addSkillAnswer = await ask(rl, "Add limits-policy-generator skill to OpenClaw workspace?", "Y");
86
+ rl.close();
87
+ if (apiToken)
88
+ await runConfigSet("apiToken", JSON.stringify(apiToken));
89
+ const sandboxYes = /^y(es)?$/i.test(sandboxAnswer.trim());
90
+ if (sandboxYes) {
91
+ const SANDBOX_ALLOW_KEY = "tools.sandbox.tools.allow";
92
+ const raw = await runConfigGet(SANDBOX_ALLOW_KEY);
93
+ let allow = [];
94
+ if (raw) {
95
+ try {
96
+ const parsed = JSON.parse(raw);
97
+ allow = Array.isArray(parsed) ? parsed : [];
98
+ }
99
+ catch {
100
+ allow = [];
101
+ }
102
+ }
103
+ if (allow.includes("limits-openclaw")) {
104
+ console.log("\nlimits-openclaw is already in tools.sandbox.tools.allow, skipping.");
105
+ }
106
+ else {
107
+ allow.push("limits-openclaw");
108
+ await runConfigSetFull(SANDBOX_ALLOW_KEY, JSON.stringify(allow));
109
+ console.log("\nAdded limits-openclaw to tools.sandbox.tools.allow.");
110
+ }
111
+ }
112
+ const addSkillYes = /^y(es)?$/i.test(addSkillAnswer.trim());
113
+ if (addSkillYes)
114
+ copySkillToWorkspace();
115
+ console.log("\nDone. Restart the gateway if it is running.");
116
+ console.log('Verify: openclaw config get plugins.entries["limits-openclaw"]\n');
117
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * SaaS HTTP client: POST /openclaw/enforce with timeout and retries.
3
+ * Never logs request body or apiToken.
4
+ */
5
+ const RETRY_DELAYS_MS = [200, 400];
6
+ function isRetryableStatus(status) {
7
+ return status === 429 || (status >= 500 && status < 600);
8
+ }
9
+ export async function callEnforce(config, body) {
10
+ const url = `${config.baseUrl.replace(/\/$/, "")}/openclaw/enforce`;
11
+ for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt++) {
12
+ const controller = new AbortController();
13
+ const timeoutId = setTimeout(() => controller.abort(), config.timeoutMs);
14
+ try {
15
+ const res = await fetch(url, {
16
+ method: "POST",
17
+ headers: { "Content-Type": "application/json" },
18
+ body: JSON.stringify(body),
19
+ signal: controller.signal,
20
+ });
21
+ clearTimeout(timeoutId);
22
+ if (!res.ok) {
23
+ if (isRetryableStatus(res.status) && attempt < RETRY_DELAYS_MS.length) {
24
+ await new Promise((r) => setTimeout(r, RETRY_DELAYS_MS[attempt] ?? 0));
25
+ continue;
26
+ }
27
+ return null;
28
+ }
29
+ const data = (await res.json());
30
+ if (data &&
31
+ typeof data === "object" &&
32
+ "action" in data &&
33
+ typeof data.action === "string") {
34
+ return data;
35
+ }
36
+ return null;
37
+ }
38
+ catch (err) {
39
+ clearTimeout(timeoutId);
40
+ const isAbort = err.name === "AbortError";
41
+ if (!isAbort && attempt < RETRY_DELAYS_MS.length) {
42
+ await new Promise((r) => setTimeout(r, RETRY_DELAYS_MS[attempt] ?? 0));
43
+ continue;
44
+ }
45
+ return null;
46
+ }
47
+ }
48
+ return null;
49
+ }