Skill: leventsoft/portainer-skill-openclaw (version 0.1.1)

--- name: portainer description: Comprehensive management for Portainer CE environments and stacks. Supports listing environments, managing Docker Compose/Swarm stacks, and executing raw Docker commands via proxy. Use when the user needs to deploy apps, check container status, or manage networks within Portainer. Requires a Portainer API Key configured in OpenClaw. --- # Portainer Manager Skill Manage your Docker infrastructure through the Portainer CE HTTP API. ## Setup Add your Portainer API Key to your OpenClaw configuration: ```bash openclaw config set portainer.apiKey "your_token_here" ``` ## Functions * `list_environments()`: Retrieves all Portainer environments (endpoints). * `list_stacks(environment_id)`: Lists all stacks. Optional: filter by environment ID. * `inspect_stack(stack_id)`: Returns full JSON details for a specific stack. * `deploy_stack(stack_name, compose_content, environment_id)`: Launches a new Docker Compose stack from a string. * `remove_stack(stack_id)`: Deletes a stack by ID. * `execute_docker_command(environment_id, path, method, payload)`: Advanced. Proxies raw Docker API requests (e.g., `/containers/json`) through Portainer.

path: README.md
content: # Portainer Skill for OpenClaw

A powerful OpenClaw skill that allows you to manage your Docker infrastructure directly through your Portainer CE instance. Deploy stacks, inspect containers, and execute raw Docker commands without leaving your chat.

## Requirements

This skill uses a Python script to communicate with the Portainer API. You must ensure your OpenClaw environment has Python 3 installed along with the `requests` library.

### Dockerfile Setup
If you are building a custom OpenClaw image (recommended for persistence), add the following to your `Dockerfile`:

```dockerfile
# Install Python 3 and pip
RUN apt-get update && apt-get install -y python3 python3-pip

# Install required Python libraries
RUN pip3 install requests --break-system-packages
```

*(Note: `--break-system-packages` is often required on newer Debian/Ubuntu versions like Bookworm to install packages globally via pip. Alternatively, use a virtual environment.)*

## Features

- **Environment Management**: List and query all your Portainer environments (endpoints).
- **Stack Operations**:
  - List all running stacks across environments.
  - Inspect detailed configuration of specific stacks.
  - **Deploy new stacks** from raw Docker Compose content.
  - Remove stacks safely.
- **Advanced Control**: Execute raw Docker API commands via Portainer's proxy (e.g., restart containers, check logs, inspect networks).

## Prerequisites

- An active [Portainer CE](https://www.portainer.io/) instance.
- An **API Access Token** from your Portainer user settings.
- OpenClaw installed and running.

## Installation

### 1. Import the Skill
You can install this skill directly from GitHub:
```bash
openclaw skill install https://github.com/Leventsoft/portainer-skill-openclaw
```

### 2. Configure Authentication
Set your Portainer URL and API Key in the OpenClaw configuration:

```bash
# Set your Portainer URL (include protocol and port if necessary)
openclaw config set portainer.url "https://portainer.yourdomain.com"

# Set your API Access Token
openclaw config set portainer.apiKey "ptr_..."
```

## Usage

Once installed, you can ask OpenClaw to perform tasks like:

> "List all my running stacks on the home-server environment."
> "Deploy a new nginx stack called 'web-proxy' using this compose file..."
> "Restart the 'database' container on environment 2."

### Available Functions

| Function | Description |
|----------|-------------|
| `list_environments()` | Retrieves a list of all connected Portainer environments/endpoints. |
| `list_stacks(environment_id)` | Lists all stacks, optionally filtered by a specific Environment ID. |
| `inspect_stack(stack_id)` | Returns full JSON details for a specific stack (services, env vars, etc). |
| `deploy_stack(stack_name, compose_content, environment_id)` | Deploys a new stack using the provided Docker Compose YAML string. |
| `remove_stack(stack_id)` | Permanently removes a stack and its associated resources. |
| `execute_docker_command(environment_id, path, method, payload)` | Proxies a raw Docker API request through Portainer (e.g., `POST /containers/my-id/restart`). |

## Security Note

This skill requires an API Key with sufficient privileges to manage your Docker environments. Ensure your OpenClaw instance is secured, as this skill provides powerful access to your infrastructure.

## License

MIT


path: package.json
content: {
  "name": "portainer",
  "version": "1.0.0",
  "description": "Manages Portainer CE environments and stacks via HTTP API.",
  "dependencies": {
    "requests": "^2.31.0",
    "urllib3": "^2.0.0"
  },
  "openclaw": {
    "entrypoint": "scripts/portainer_manager.py"
  }
}


path: scripts/portainer_manager.py
content: # Portainer Manager Skill Backend

import requests
import os
import json
import sys
import traceback
import urllib3

# Attempt to get Portainer API URL from environment, default to https://localhost:9443/api
PORTAINER_API_URL = os.environ.get("PORTAINER_API_URL", "https://localhost:9443/api") 

# Suppress warnings for self-signed certificates
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def get_portainer_api_token():
    [REDACTED_SECRET]"PORTAINER_API_TOKEN")
    if not token:
        raise ValueError("PORTAINER_API_TOKEN environment variable not set.")
    return token

def list_environments():
    print(f"Attempting to list Portainer environments via {PORTAINER_API_URL}...", flush=True)
    try:
        [REDACTED_SECRET])
        headers = {"X-API-Key": token, "Content-Type": "application/json"}
        response = requests.get(f"{PORTAINER_API_URL}/endpoints", headers=headers, timeout=10, verify=False)
        response.raise_for_status()
        output_data = response.json()
        print(json.dumps(output_data, indent=2), flush=True)
        return output_data
    except Exception as e:
        print(f"ERROR: {e}", flush=True)
        return {"error": str(e)}

def list_stacks(environment_id=None):
    print(f"Executing list_stacks{' for environment ID: ' + str(environment_id) if environment_id else ''}...", flush=True)
    try:
        [REDACTED_SECRET])
        headers = {"X-API-Key": token, "Content-Type": "application/json"}
        response = requests.get(f"{PORTAINER_API_URL}/stacks", headers=headers, timeout=10, verify=False)
        response.raise_for_status()
        stacks = response.json()
        if environment_id:
            env_id_int = int(environment_id)
            stacks = [s for s in stacks if s.get("EndpointId") == env_id_int]
        print(json.dumps(stacks, indent=2), flush=True)
        return stacks
    except Exception as e:
        print(f"ERROR: {e}", flush=True)
        return {"error": str(e)}

def inspect_stack(stack_id):
    print(f"Inspecting stack ID: {stack_id}...", flush=True)
    try:
        [REDACTED_SECRET])
        headers = {"X-API-Key": token, "Content-Type": "application/json"}
        response = requests.get(f"{PORTAINER_API_URL}/stacks/{stack_id}", headers=headers, timeout=10, verify=False)
        response.raise_for_status()
        output_data = response.json()
        
        # Also fetch the stack file content if possible
        try:
            file_response = requests.get(f"{PORTAINER_API_URL}/stacks/{stack_id}/file", headers=headers, timeout=10, verify=False)
            if file_response.status_code == 200:
                output_data["StackFileContent"] = file_response.json().get("StackFileContent", "")
        except:
            pass

        print(json.dumps(output_data, indent=2), flush=True)
        return output_data
    except Exception as e:
        print(f"ERROR: {e}", flush=True)
        return {"error": str(e)}

def remove_stack(stack_id):
    print(f"Removing stack ID: {stack_id}...", flush=True)
    try:
        [REDACTED_SECRET])
        headers = {"X-API-Key": token, "Content-Type": "application/json"}
        response = requests.delete(f"{PORTAINER_API_URL}/stacks/{stack_id}", headers=headers, timeout=10, verify=False)
        response.raise_for_status()
        print(f"Successfully removed stack {stack_id}.", flush=True)
        return {"status": "success"}
    except Exception as e:
        print(f"ERROR: {e}", flush=True)
        return {"error": str(e)}

def deploy_stack(name, stack_content, endpoint_id):
    print(f"Deploying stack '{name}' to environment ID {endpoint_id}...", flush=True)
    try:
        [REDACTED_SECRET])
        headers = {"X-API-Key": token, "Content-Type": "application/json"}
        url = f"{PORTAINER_API_URL}/stacks/create/standalone/string?endpointId={endpoint_id}"
        payload = {"name": name, "stackFileContent": stack_content, "env": []}
        response = requests.post(url, headers=headers, json=payload, timeout=30, verify=False)
        response.raise_for_status()
        output_data = response.json()
        print(json.dumps(output_data, indent=2), flush=True)
        return output_data
    except Exception as e:
        print(f"ERROR: {e}", flush=True)
        return {"error": str(e)}

def update_stack(stack_id, stack_content, endpoint_id, prune=False):
    print(f"Updating stack ID {stack_id}...", flush=True)
    try:
        [REDACTED_SECRET])
        headers = {"X-API-Key": token, "Content-Type": "application/json"}
        url = f"{PORTAINER_API_URL}/stacks/{stack_id}?endpointId={endpoint_id}"
        payload = {
            "stackFileContent": stack_content,
            "env": [],
            "prune": prune
        }
        response = requests.put(url, headers=headers, json=payload, timeout=30, verify=False)
        response.raise_for_status()
        output_data = response.json()
        print(json.dumps(output_data, indent=2), flush=True)
        return output_data
    except Exception as e:
        print(f"ERROR: {e}", flush=True)
        return {"error": str(e)}

def execute_docker_command(environment_id, path, method="GET", payload=None):
    print(f"Executing Docker command on env {environment_id}: {method} {path}...", flush=True)
    try:
        [REDACTED_SECRET])
        headers = {"X-API-Key": token, "Content-Type": "application/json"}
        # Portainer proxies Docker API requests at /endpoints/{id}/docker/{path}
        url = f"{PORTAINER_API_URL}/endpoints/{environment_id}/docker{path}"
        
        if method.upper() == "GET":
            response = requests.get(url, headers=headers, params=payload, timeout=10, verify=False)
        elif method.upper() == "POST":
            response = requests.post(url, headers=headers, json=payload, timeout=10, verify=False)
        elif method.upper() == "DELETE":
            response = requests.delete(url, headers=headers, json=payload, timeout=10, verify=False)
        else:
            raise ValueError(f"Unsupported method: {method}")

        response.raise_for_status()
        
        try:
            output_data = response.json()
        except:
            output_data = {"status": "success", "text": response.text}
            
        print(json.dumps(output_data, indent=2), flush=True)
        return output_data
    except Exception as e:
        print(f"ERROR: {e}", flush=True)
        return {"error": str(e)}

if __name__ == "__main__":
    if len(sys.argv) < 2:
        sys.exit(1)
    cmd = sys.argv[1]
    
    if cmd == "list_environments":
        list_environments()
    elif cmd == "list_stacks":
        env_id = sys.argv[2] if len(sys.argv) > 2 else None
        list_stacks(env_id)
    elif cmd == "inspect_stack":
        inspect_stack(sys.argv[2])
    elif cmd == "remove_stack":
        remove_stack(sys.argv[2])
    elif cmd == "deploy_stack":
        deploy_stack(sys.argv[2], sys.argv[3], sys.argv[4])
    elif cmd == "update_stack":
        # usage: update_stack <id> <content> <endpoint_id>
        update_stack(sys.argv[2], sys.argv[3], sys.argv[4])
    elif cmd == "execute_docker_command":
        # usage: execute_docker_command <env_id> <path> <method> <json_payload>
        env_id = sys.argv[2]
        path = sys.argv[3]
        method = sys.argv[4] if len(sys.argv) > 4 else "GET"
        payload = json.loads(sys.argv[5]) if len(sys.argv) > 5 else None
        execute_docker_command(env_id, path, method, payload)


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