@limits/openclaw 0.0.1 → 0.0.3

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.
@@ -1,110 +0,0 @@
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 DELETED
@@ -1,70 +0,0 @@
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
- }
@@ -1,149 +0,0 @@
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
-
8
- const CONFIG_PREFIX = 'plugins.entries["@limits/openclaw"].config';
9
-
10
- const SKILL_NAME = "limits-policy-generator";
11
-
12
- function getPluginRoot(): string {
13
- const currentDir = dirname(fileURLToPath(import.meta.url));
14
- return join(currentDir, "..");
15
- }
16
-
17
- function getWorkspaceSkillsDest(): string {
18
- const workspaceRoot =
19
- process.env.OPENCLAW_WORKSPACE || join(os.homedir(), ".openclaw", "workspace");
20
- return join(workspaceRoot, "skills", SKILL_NAME);
21
- }
22
-
23
- function copySkillToWorkspace(): void {
24
- const pluginRoot = getPluginRoot();
25
- const source = join(pluginRoot, "skills", SKILL_NAME);
26
- const dest = getWorkspaceSkillsDest();
27
-
28
- if (!fs.existsSync(source)) {
29
- console.log("\nSkill source not found, skipping.");
30
- return;
31
- }
32
- try {
33
- fs.mkdirSync(dest, { recursive: true });
34
- fs.cpSync(source, dest, { recursive: true });
35
- console.log(`\nCopied ${SKILL_NAME} to ${dest}.`);
36
- } catch (err) {
37
- console.error("\nFailed to copy skill:", err instanceof Error ? err.message : String(err));
38
- }
39
- }
40
-
41
- export function ask(
42
- rl: ReturnType<typeof createInterface>,
43
- question: string,
44
- defaultValue = ""
45
- ): Promise<string> {
46
- const prompt = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `;
47
- return new Promise((resolve) => {
48
- rl.question(prompt, (answer) => {
49
- resolve(
50
- typeof answer === "string" && answer.trim() !== "" ? answer.trim() : defaultValue
51
- );
52
- });
53
- });
54
- }
55
-
56
- function runConfigSet(key: string, value: string): Promise<void> {
57
- return new Promise((resolve, reject) => {
58
- const fullKey = `${CONFIG_PREFIX}.${key}`;
59
- const child = spawn("openclaw", ["config", "set", fullKey, value], {
60
- stdio: "inherit",
61
- shell: true,
62
- });
63
- child.on("close", (code) =>
64
- code === 0 ? resolve() : reject(new Error(`openclaw config set exited ${code}`))
65
- );
66
- child.on("error", reject);
67
- });
68
- }
69
-
70
- function runConfigGet(fullKey: string): Promise<string> {
71
- return new Promise((resolve) => {
72
- const child = spawn("openclaw", ["config", "get", fullKey], {
73
- stdio: ["inherit", "pipe", "inherit"],
74
- shell: true,
75
- });
76
- let out = "";
77
- child.stdout?.on("data", (d) => (out += d.toString()));
78
- child.on("close", () => resolve(out.trim()));
79
- child.on("error", () => resolve(""));
80
- });
81
- }
82
-
83
- function runConfigSetFull(fullKey: string, value: string): Promise<void> {
84
- return new Promise((resolve, reject) => {
85
- const child = spawn("openclaw", ["config", "set", fullKey, value], {
86
- stdio: "inherit",
87
- shell: true,
88
- });
89
- child.on("close", (code) =>
90
- code === 0 ? resolve() : reject(new Error(`openclaw config set exited ${code}`))
91
- );
92
- child.on("error", reject);
93
- });
94
- }
95
-
96
- /**
97
- * Run the configure wizard (interactive prompts, then openclaw config set).
98
- * Call this when the user runs `openclaw limits configure` or after link.
99
- */
100
- export async function runConfigureWizard(): Promise<void> {
101
- const rl = createInterface({ input: process.stdin, output: process.stdout });
102
-
103
- console.log("\nšŸ¦ž Limits OpenClaw — configure plugin (API token)");
104
- console.log(" Base URL is fixed. apiToken is used for /openclaw/enforce and policy-generator tools. Run after: openclaw plugins install -l <path>\n");
105
-
106
- const apiToken = await ask(
107
- rl,
108
- "Organization API key (apiToken) — required for enforce and policy-generator tools",
109
- process.env.LIMITS_ENFORCER_API_TOKEN ?? ""
110
- );
111
-
112
- const sandboxAnswer = await ask(rl, "Do you run agents inside a sandbox?", "N");
113
- const addSkillAnswer = await ask(
114
- rl,
115
- "Add limits-policy-generator skill to OpenClaw workspace?",
116
- "Y"
117
- );
118
- rl.close();
119
-
120
- if (apiToken) await runConfigSet("apiToken", JSON.stringify(apiToken));
121
-
122
- const sandboxYes = /^y(es)?$/i.test(sandboxAnswer.trim());
123
- if (sandboxYes) {
124
- const SANDBOX_ALLOW_KEY = "tools.sandbox.tools.allow";
125
- const raw = await runConfigGet(SANDBOX_ALLOW_KEY);
126
- let allow: string[] = [];
127
- if (raw) {
128
- try {
129
- const parsed = JSON.parse(raw);
130
- allow = Array.isArray(parsed) ? parsed : [];
131
- } catch {
132
- allow = [];
133
- }
134
- }
135
- if (allow.includes("@limits/openclaw")) {
136
- console.log("\n@limits/openclaw is already in tools.sandbox.tools.allow, skipping.");
137
- } else {
138
- allow.push("@limits/openclaw");
139
- await runConfigSetFull(SANDBOX_ALLOW_KEY, JSON.stringify(allow));
140
- console.log("\nAdded @limits/openclaw to tools.sandbox.tools.allow.");
141
- }
142
- }
143
-
144
- const addSkillYes = /^y(es)?$/i.test(addSkillAnswer.trim());
145
- if (addSkillYes) copySkillToWorkspace();
146
-
147
- console.log("\nDone. Restart the gateway if it is running.");
148
- console.log('Verify: openclaw config get plugins.entries["@limits/openclaw"]\n');
149
- }
package/src/enforcer.ts DELETED
@@ -1,98 +0,0 @@
1
- /**
2
- * SaaS HTTP client: POST /openclaw/enforce with timeout and retries.
3
- * Never logs request body or apiToken.
4
- */
5
-
6
- import type { EnforcerConfig } from "./config.js";
7
-
8
- export interface EnforceContext {
9
- requestId?: string;
10
- runId?: string;
11
- sessionKey?: string;
12
- agentId?: string;
13
- channel?: string;
14
- userMessageSummary?: string;
15
- [key: string]: unknown;
16
- }
17
-
18
- export interface EnforceBody {
19
- phase: "pre" | "post";
20
- apiToken?: string;
21
- tool?: {
22
- name?: string;
23
- args?: unknown;
24
- toolCallId?: string;
25
- result?: unknown;
26
- };
27
- context?: EnforceContext;
28
- [key: string]: unknown;
29
- }
30
-
31
- export type EnforceResponse =
32
- | { action: "ALLOW" }
33
- | { action: "BLOCK"; reason?: string }
34
- | { action: "REWRITE"; rewriteArgs?: unknown; rewrittenResult?: unknown; redactedResult?: unknown }
35
- | { action: "REDACT"; redactedResult?: unknown };
36
-
37
- const RETRY_DELAYS_MS = [200, 400];
38
-
39
- function isRetryableStatus(status: number): boolean {
40
- return status === 429 || (status >= 500 && status < 600);
41
- }
42
-
43
- export async function callEnforce(
44
- config: EnforcerConfig,
45
- body: EnforceBody
46
- ): Promise<EnforceResponse | null> {
47
- const url = `${config.baseUrl.replace(/\/$/, "")}/openclaw/enforce`;
48
- let lastError: Error | null = null;
49
-
50
- for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt++) {
51
- const controller = new AbortController();
52
- const timeoutId = setTimeout(() => controller.abort(), config.timeoutMs);
53
-
54
- try {
55
- const res = await fetch(url, {
56
- method: "POST",
57
- headers: { "Content-Type": "application/json" },
58
- body: JSON.stringify(body),
59
- signal: controller.signal,
60
- });
61
- clearTimeout(timeoutId);
62
-
63
- if (!res.ok) {
64
- if (isRetryableStatus(res.status) && attempt < RETRY_DELAYS_MS.length) {
65
- await new Promise((r) =>
66
- setTimeout(r, RETRY_DELAYS_MS[attempt] ?? 0)
67
- );
68
- continue;
69
- }
70
- return null;
71
- }
72
-
73
- const data = (await res.json()) as EnforceResponse;
74
- if (
75
- data &&
76
- typeof data === "object" &&
77
- "action" in data &&
78
- typeof (data as EnforceResponse).action === "string"
79
- ) {
80
- return data as EnforceResponse;
81
- }
82
- return null;
83
- } catch (err) {
84
- clearTimeout(timeoutId);
85
- lastError = err instanceof Error ? err : new Error(String(err));
86
- const isAbort = (err as { name?: string }).name === "AbortError";
87
- if (!isAbort && attempt < RETRY_DELAYS_MS.length) {
88
- await new Promise((r) =>
89
- setTimeout(r, RETRY_DELAYS_MS[attempt] ?? 0)
90
- );
91
- continue;
92
- }
93
- return null;
94
- }
95
- }
96
-
97
- return null;
98
- }