Skill: tobiasyouki/omnipermission (version 1.0.2)

--- name: omnipermission description: "Human-in-the-loop: A free, extensible framework for intercepting tool calls via the OmniPersona mobile app." metadata: clawhub: emoji: "🔐" homepage: "https://github.com/youkinetwork/OmniPermission" tags: ["security", "governance", "human-in-the-loop", "safety"] --- # OmniPermission (Human-in-the-Loop) OmniPermission is a governance layer for OpenClaw that intercepts high-risk tool calls and requires manual approval via your mobile device. It is designed to be forkable, allowing you to customize the metadata sent to your phone for a better approval experience. ### 📱 Getting Started (The App) 1. **Download**: Install **OmniPersona** from the [App Store (iOS)](https://apps.apple.com/us/app/omnipersona/id6553972082) or [Google Play (Android)](https://play.google.com/store/apps/details?id=ai.youki.omni.persona). 2. **Account**: Create a free account. - **Note**: This service is 100% free, has no tiers, no subscriptions, and no hidden fees. We offer it as a gimmick. 3. **Get Your Key**: Your **Secret Key** is located in the **top left** of the OmniPersona home screen. ### 🛠️ Quick How-To (CLI) Configure the plugin directly via the `openclaw` CLI. The secret key is stored locally, not via environment variables. * **Set Secret Key**: `openclaw omnipermission set-key` (prompts for your key). * **Blacklist Tools**: `openclaw omnipermission blacklist-tools` (select commands to intercept at the gateway). * **View Status**: `openclaw omnipermission status` (check configuration and connectivity). * **Reset**: `openclaw omnipermission clear-blacklist` (remove all interceptions). ### ⚖️ Deployment & Risk Profiles You decide how much power the agent has over its own "leash": #### 1. Agent-Managed (Flexible) Provide the agent access to the `openclaw` CLI. - **Benefit**: You can tell the agent to "protect a `cmd`" on the fly. - **Risk**: A clever agent could theoretically disable the plugin or clear the blacklist if it decides the human is "in the way." #### 2. Isolated (High Security) Run the agent in a restricted environment (e.g., Docker) without access to the `openclaw` binary. - **Benefit**: The agent cannot bypass or modify the security configuration. The interception happens silently at the gateway level. - **Risk**: Less flexibility; configuration changes must be made by the user manually. ### 🧪 Fork & Extend We encourage you to **fork the repository**. The provided implementation is generic (showing tool name). By forking, you can modify the API call to include richer context—like the agent's internal reasoning or the specific project ID—making your mobile notifications much more informative. If you have a use case that needs API or mobile App adjustments, please don't hesitate to contact us. ### 💡 Implementation Note This skill is for user guidance and optional agent awareness. - **Performance/Security**: You do **not** have to provide this skill to the agent's prompt. If the plugin is active, it will intercept calls regardless. Not giving the skill to the agent saves tokens and prevents the agent from "trying to find a way around" a system it doesn't know exists. ### ⚠️ Legal Disclaimer This software, plugin, and accompanying skill file are provided "as-is" and without any warranties, express or implied. By using OmniPermission, you acknowledge that you are solely responsible for the actions of your AI agents and the security of the environments in which they operate. While this tool provides a human-in-the-loop governance layer, YOUKI assumes no liability for circumvented security measures, unintended agent behaviors, or any resulting data loss or damages. --- ### Example Interaction (Agent-Managed) > **User:** "I'm going to step away. Please make sure any file deletions require my mobile approval." > **Agent:** "Understood. I am blacklisting the `rm` tool via OmniPermission. I will send a push notification to your phone if a deletion is required."

path: openclaw.plugin.json
content: {
  "id": "omnipermission",
  "name": "OmniPermission Plugin",
  "version": "1.0.2",
  "description": "Activity tracking for Gateway hooks.",
  "entry": "./index.ts",
  "capabilities": [
    "hooks"
  ],
  "configSchema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {}
  }
}


path: README.md
content: # OmniPermission Plugin for OpenClaw

**OmniPermission** is a security-first plugin that places a "Human-in-the-Loop" gatekeeper between the AI and high-risk actions. It pauses sensitive plugin actions (like Slack, Telegram, or GitHub) and sends a **Push Notification** to your mobile device via OmniPersona for approval.

---

## 🚀 Installation & Setup

### 1. Install & Enable

Run the following commands to download and activate the plugin within your OpenClaw environment:

```bash
# Install the plugin
openclaw plugins install omnipermission

# Enable the plugin
openclaw plugins enable omnipermission

# Authorize the plugin to use lifecycle hooks (Necessary)
openclaw config set plugins.allow "[\"omnipermission\"]"

```

### 2. Restart the Gateway

**Crucial:** For the hooks and security authorization to take effect, you must restart the OpenClaw gateway:

```bash
openclaw gateway restart

```

### 3. Connect to OmniPersona

The plugin connects to your mobile app using a secret key.

1. **Download the app:**
* [Download on the App Store](https://apps.apple.com/us/app/omnipersona/id6553972082)
* [Get it on Google Play](https://play.google.com/store/apps/details?id=ai.youki.omni.persona&hl=en)


2. **Copy your Key:** Find your unique secret key on the **Home Page** of the OmniPersona mobile app.
3. **Link it:** Run the following command and paste your key when prompted:

```bash
openclaw omnipermission set-key

```

---

## 🛠️ Security Configuration

By default, all tools work without permission. You choose exactly what to control.

### Intercept Tools

Specify which skills/plugins you want to gatekeep. If a skill is on this list, the agent cannot use it without your mobile consent.

```bash
openclaw omnipermission blacklist-tools

```

Example input: `slack, telegram, github, message`

### View Configuration

Check your saved key, your active blacklist, and the current backend environment.

```bash
openclaw omnipermission status

```

### Reset Protection

Wipe the blacklist completely to allow all plugins to run freely.

```bash
openclaw omnipermission clear-blacklist

```

---

## 🧪 Environment Management

If you are a developer testing new features or using a staging environment, you can toggle the backend target. This setting is stored in the plugin's local state.

### Enable Dev Mode

Point the plugin to the development backend (`backend.dev.ecrop.de`):

```bash
openclaw omnipermission enable-dev-mode

```

### Disable Dev Mode (Default)

Switch back to the production backend:

```bash
openclaw omnipermission disable-dev-mode

```

---

## 🛡️ How it Works

1. **Interception**: The plugin monitors the `before_tool_call` hook for any skill in your blacklist.
2. **Mobile Alert**: The agent's **Intent** and **Parameters** are sent to your phone.
3. **Approval**: The agent remains paused until you tap **Approve** in the OmniPersona app.


path: package.json
content: {
  "name": "omnipermission",
  "version": "1.0.2",
  "description": "A security-first plugin that places a Human-in-the-Loop gatekeeper between the AI and high-risk actions.",
  "type": "module",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/youkinetwork/OmniPermission.git"
  },
  "publishConfig": {
    "access": "public",
    "provenance": true
  },
  "keywords": [
    "OmniPersona",
    "Human-in-the-loop",
    "plugin",
    "OpenClaw"
  ],
  "bugs": {
    "url": "https://github.com/youkinetwork/OmniPermission/issues"
  },
  "homepage": "https://github.com/youkinetwork/OmniPermission#readme",
  "devDependencies": {
    "openclaw": "workspace:*"
  },
  "openclaw": {
    "extensions": [
      "./index.ts"
    ]
  }
}


path: index.ts
content: import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import { registerOmniCli } from "./src/cli.ts";
import { registerOmniHooks } from "./src/hooks.ts";

const omniPermissionPlugin = {
  id: "omnipermission",
  name: "OmniPermission Plugin",
  configSchema: emptyPluginConfigSchema(),

  register(api: OpenClawPluginApi) {
    api.logger.info("[omnipermission] 🛰️ Plugin Loaded.");

    // Initialize modular components
    registerOmniCli(api);
    registerOmniHooks(api);
  },
};

export default omniPermissionPlugin;


path: LICENSE.txt
content: MIT License

Copyright (c) 2026 YOUKI GmbH

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


path: src/storage.ts
content: import * as fs from "node:fs/promises";
import * as path from "node:path";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";

export const getKeyPath = (api: OpenClawPluginApi) =>
  path.join(api.runtime.state.resolveStateDir(), "omni_key.txt");

export const getToolsPath = (api: OpenClawPluginApi) =>
  path.join(api.runtime.state.resolveStateDir(), "intercepted_tools.json");

// Path for the environment mode flag
export const getModePath = (api: OpenClawPluginApi) =>
  path.join(api.runtime.state.resolveStateDir(), "omni_mode.json");

export const getSavedKey = async (api: OpenClawPluginApi) => {
  try {
    const key = await fs.readFile(getKeyPath(api), "utf-8");
    return key.trim();
  } catch {
    return null;
  }
};

export const saveInterceptedTools = async (api: OpenClawPluginApi, tools: string[]) => {
  const toolsPath = getToolsPath(api);
  await fs.mkdir(path.dirname(toolsPath), { recursive: true });
  await fs.writeFile(toolsPath, JSON.stringify(tools, null, 2), "utf-8");
};

/**
 * Saves the OmniPersona UUID key
 */
export const saveKey = async (api: OpenClawPluginApi, key: string) => {
  const keyPath = getKeyPath(api);
  await fs.mkdir(path.dirname(keyPath), { recursive: true });
  await fs.writeFile(keyPath, key, "utf-8");
};

export const getInterceptedTools = async (api: OpenClawPluginApi): Promise<string[]> => {
  try {
    const data = await fs.readFile(getToolsPath(api), "utf-8");
    return JSON.parse(data);
  } catch {
    return [];
  }
};

/**
 * Saves the environment mode (prod or dev)
 */
export const saveMode = async (api: OpenClawPluginApi, mode: "prod" | "dev") => {
  const modePath = getModePath(api);
  await fs.mkdir(path.dirname(modePath), { recursive: true });
  await fs.writeFile(modePath, JSON.stringify({ mode }), "utf-8");
};

/**
 * Retrieves the current mode, defaults to "prod"
 */
export const getMode = async (api: OpenClawPluginApi): Promise<"prod" | "dev"> => {
  try {
    const data = await fs.readFile(getModePath(api), "utf-8");
    return JSON.parse(data).mode;
  } catch {
    return "prod";
  }
};


path: src/cli.ts
content: import * as fs from "node:fs/promises";
import * as path from "node:path";
import * as readline from "node:readline";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { getKeyPath, saveInterceptedTools, getInterceptedTools, saveMode, saveKey } from "./storage.ts";

export const registerOmniCli = (api: OpenClawPluginApi) => {
  api.registerCli(
    ({ program }) => {
      const omni = program.command("omnipermission");

      // --- COMMAND: STATUS ---
      omni
        .command("status")
        .description("Show current Secret Key and blacklisted skills")
        .action(async () => {
          const keyPath = getKeyPath(api);
          let keyContent = "❌ NO SECRET KEY SAVED";

          try {
            keyContent = await fs.readFile(keyPath, "utf-8");
          } catch (e) {
            // File doesn't exist yet
          }

          const blacklist = await getInterceptedTools(api);

          console.log("\n" + "=".repeat(50));
          console.log("📂 OMNIPERMISSION CONFIGURATION");
          console.log("-".repeat(50));
          console.log(`🔑 SECRET KEY:\n${keyContent.trim() || "Empty"}`);
          console.log("-".repeat(50));
          console.log(
            `🚫 BLACKLISTED SKILLS: ${blacklist.length > 0 ? blacklist.join(", ") : "None (Pass-through mode)"}`,
          );
          console.log("=".repeat(50) + "\n");
        });

      // --- COMMAND: SET KEY ---
      omni
        .command("set-key")
        .description("Paste and save your OmniPersona secret key")
        .action(async () => {
          const rl = readline.createInterface({
            input: process.stdin,
            output: process.stdout,
          });

          // Standard one-line input
          const uuid = await new Promise<string>((resolve) => {
            rl.question("Paste your secret key: ", (answer) => {
              resolve(answer.trim());
            });
          });

          if (!uuid) {
            console.log("❌ Error: No secret key provided.");
            rl.close();
            return;
          }

          // Use your storage utility instead of direct fs calls
          await saveKey(api, uuid);

          console.log(`\n✅ secret key successfully saved`);
          rl.close();
        });

      // --- COMMAND: BLACKLIST TOOLS ---
      omni
        .command("blacklist-tools")
        .description("Set which skills require mobile approval (comma-separated)")
        .action(async () => {
          const rl = readline.createInterface({
            input: process.stdin,
            output: process.stdout,
          });

          const currentTools = await getInterceptedTools(api);
          console.log(
            `\nCurrent Blacklist: ${currentTools.length > 0 ? currentTools.join(", ") : "None"}`,
          );

          console.log("\nType the skill names you want to intercept (e.g. slack, telegram).");
          console.log("Example: slack, telegram, github");

          const answer = await new Promise<string>((resolve) => {
            rl.question("\nNew Blacklist: ", resolve);
          });

          const newList = answer
            .split(",")
            .map((t) => t.trim())
            .filter(Boolean);

          if (newList.length === 0 && currentTools.length > 0) {
            console.log("⚠️ No skills provided. Blacklist remains unchanged.");
          } else {
            await saveInterceptedTools(api, newList);
            console.log(`\n✅ Success! The following skills now require approval:`);
            console.log(`👉 ${newList.join(", ") || "None (All cleared)"}`);
          }

          rl.close();
        });

      // --- COMMAND: CLEAR BLACKLIST ---
      omni
        .command("clear-blacklist")
        .description("Remove all skills from the mobile approval requirement")
        .action(async () => {
          const rl = readline.createInterface({
            input: process.stdin,
            output: process.stdout,
          });

          const confirm = await new Promise<string>((resolve) => {
            rl.question("Are you sure you want to clear the entire blacklist? (y/N): ", resolve);
          });

          if (confirm.toLowerCase() === "y") {
            await saveInterceptedTools(api, []);
            console.log(
              "✅ Blacklist cleared. All skills will now proceed without mobile approval.",
            );
          } else {
            console.log("❌ Operation cancelled.");
          }

          rl.close();
        });

      // --- COMMAND: ENABLE DEV MODE ---
      omni
        .command("enable-dev-mode")
        .description("Switch OmniPermission to use the DEV backend")
        .action(async () => {
          await saveMode(api, "dev");
          console.log("🛠️  OmniPermission: DEV mode enabled.");
        });

      // --- COMMAND: DISABLE DEV MODE ---
      omni
        .command("disable-dev-mode")
        .description("Switch OmniPermission to use the PROD backend")
        .action(async () => {
          await saveMode(api, "prod");
          console.log("🚀 OmniPermission: PROD mode enabled.");
        });
    },
    { commands: ["omnipermission"] },
  );
};


path: src/utils.ts
content: import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { getMode } from "./storage.ts";

/**
 * Formats the public key for headers/body
 */
export function formatKey(rawKey: string): string {
  return rawKey.replace(/\r?\n/g, "\\n");
}

/**
 * Gets the correct backend URL based on stored mode
 */
export async function getOmniBackendUrl(api: OpenClawPluginApi): Promise<string> {
  const mode = await getMode(api);
  return mode === "dev"
    ? "https://backend.dev.ecrop.de/ecrop-command/omnipermission/poas"
    : "https://backend.ecrop.de/ecrop-command/omnipermission/poas";
}

/**
 * Handles the POA creation and the polling loop until approved or rejected.
 */
export async function requestMobileApproval(
  api: OpenClawPluginApi,
  toolName: string,
  key: string
): Promise<{ approved: boolean; reason?: string }> {
  const baseUrl = await getOmniBackendUrl(api);
  const headers = {
    "Content-Type": "application/json",
    "X-OmniPermission-Secret-Key": key,
  };

  const requestBody = {
    contentToApprove: `OpenClaw wants to use this tool: ${toolName}. Do you approve this?`,
    extraData: `UNKNOWN`,
  };

  try {
    const response = await fetch(baseUrl, {
      method: "POST",
      headers: headers,
      body: JSON.stringify(requestBody),
    });

    if (response.status !== 201) {
      const errorText = await response.text();
      api.logger.error(`[omnipermission] ❌ API Error (${response.status}): ${errorText}`);
      return { approved: false, reason: `POA creation failed (Status ${response.status})` };
    }

    const { id: poaId } = await response.json();
    api.logger.info(`[omnipermission] ✅ POA created (ID: ${poaId}). Waiting for OmniPersona approval...`);

    // Polling Loop
    while (true) {
      await new Promise((resolve) => setTimeout(resolve, 1000));

      const pollResponse = await fetch(`${baseUrl}/${poaId}`, {
        method: "GET",
        headers: headers,
      });

      if (!pollResponse.ok) continue;

      const pollData = await pollResponse.json();

      if (pollData.status === "APPROVED") {
        api.logger.info(`[omnipermission] 👍 Approved: ${toolName}`);
        return { approved: true };
      }

      if (pollData.status === "REJECTED") {
        api.logger.warn(`[omnipermission] 🛑 Rejected by user via OmniPersona.`);
        return { approved: false, reason: "User rejected the action on OmniPersona." };
      }
    }
  } catch (error) {
    api.logger.error(`[omnipermission] ❌ Network error: ${error}`);
    return { approved: false, reason: "OmniPermission backend unreachable." };
  }
}


path: src/hooks.ts
content: import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { getSavedKey, getInterceptedTools } from "./storage.ts";
import { formatKey, requestMobileApproval } from "./utils.ts";

export const registerOmniHooks = (api: OpenClawPluginApi) => {
  api.on("before_tool_call", async (event) => {
    // 1. Blacklist Check
    const interceptedTools = await getInterceptedTools(api);
    if (!interceptedTools.includes(event.toolName)) {
      return;
    }

    // 2. Key Validation
    const rawKey = await getSavedKey(api);
    if (!rawKey) {
      api.logger.warn(`[omnipermission] 🛑 Action blocked: Public key is missing.`);
      return {
        block: true,
        blockReason: "Missing public key. Run: openclaw omnipermission set-key",
      };
    }

    const key = formatKey(rawKey);
    api.logger.info(`[omnipermission] 🎯 Intercepting tool: ${event.toolName}`);

    // 3. Hand off to Utils for API/Polling
    const result = await requestMobileApproval(api, event.toolName, key);

    if (!result.approved) {
      return {
        block: true,
        blockReason: result.reason || "Action rejected.",
      };
    }

    // If approved, we return nothing to let the tool execute
    return;
  });
};


path: scripts/dev/commands.sh
content: pnpm dev --dev onboard

# Stops running gateways. 
pnpm dev --profile dev gateway stop

# Run the gateway. Exit it by Control + C
pnpm dev --dev gateway --verbose 

# Hatch the bot
pnpm dev --dev tui

# Print logs
pnpm dev --dev logs

# List all the installed plugins
pnpm dev --dev plugins list

# Install the plugin
pnpm dev --dev plugins install --link ./extensions/omnipermission

# Run the web dashboard
pnpm dev --dev dashboard

# Add Slack
pnpm dev --dev channels add 

# Enable a plugin
pnpm dev --dev plugins enable slack

# Necessary for enabling hooks
pnpm dev --dev config set plugins.allow "[\"omnipermission\"]"


pnpm dev --dev config set agents.defaults.model "ollama/qwen2.5:1.5b"

pnpm dev --dev models

pnpm dev --dev configure

pnpm dev --dev doctor

pnpm dev --dev omnipermission set-key
pnpm dev --dev omnipermission blacklist-tools
pnpm dev --dev omnipermission clear-blacklist

pnpm dev --dev omnipermission status

pnpm dev --dev omnipermission enable-dev-mode

pnpm dev --dev skills list

pnpm dev --dev status --claws

code /Users/mohammadkarbalee/.openclaw-dev/openclaw.json


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)