Initial commit: Docker Sandbox Runner complete setup

This commit is contained in:
LumbarJam
2025-12-09 15:41:17 -03:00
commit dabc1b9bd7
16 changed files with 764 additions and 0 deletions
Vendored Executable
BIN
View File
Binary file not shown.
Executable
BIN
View File
Binary file not shown.
Executable
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+128
View File
@@ -0,0 +1,128 @@
# Docker Sandbox Runner for ComfyUI
This **Custom Node** provides a secure, isolated execution environment for Python scripts within ComfyUI, utilizing persistent Docker containers. Code execution is ultra-fast (milliseconds) and completely isolated from the host operating system.
-----
## Key Features
* **Maximum Security (Docker Isolation):** Code runs in an ephemeral Docker container, blocked from accessing the Host's filesystem, network, or sensitive resources.
* **Performance (Sidecar Pattern):** The container is started only once (the *Sidecar* pattern) and reused via `docker exec`, eliminating Docker boot time latency.
* **Dynamic Inputs:** Allows adding execution inputs (`*`, `STRING`, `INT`, etc.) dynamically via the context menu.
* **Multiple Outputs:** Provides 6 result outputs (`r1` to `r6`) and 1 log output (`log`).
* **Non-Root Execution:** Code runs as the **`nobody`** user inside the container, significantly minimizing internal security risk.
-----
## Installation
### 1\. Mandatory Prerequisites
You must have two pieces of software installed:
* **Docker Desktop/Engine:** The containerization engine itself.
* **`docker-py`:** The Python library to control Docker.
Execute the following command in your ComfyUI Python environment:
```bash
# Installs the Python library required to communicate with the Docker daemon
pip install docker
```
### 2\. Node Installation
1. Clone this repository into your ComfyUI's `custom_nodes` folder:
```bash
cd path/to/ComfyUI/custom_nodes/
git clone [Your Repository URL]
```
2. Restart ComfyUI.
-----
## Docker Engine Installation (Detailed CLI Instructions)
You must install and start the Docker service (Daemon) before using the node. **Note:** After installation, ensure your user is added to the `docker` group to run commands without `sudo` (e.g., `sudo usermod -aG docker $USER`).
| OS / Distribution | Installation Command Set (Root/Sudo Required) | Verification & Service |
| :--- | :--- | :--- |
| **Ubuntu/Debian** | **1. Install Prerequisites:**<br>`sudo apt update && sudo apt install ca-certificates curl gnupg`<br>**2. Add GPG Key:**<br>`sudo install -m 0755 -d /etc/apt/keyrings`<br>`curl -fsSL https://download.docker.com/linux/ubuntu/gpg \| sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg`<br>**3. Add Repository:**<br>`echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" \| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null`<br>**4. Install Docker:**<br>`sudo apt update && sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin` | `sudo systemctl enable docker --now` |
| **Arch/Manjaro** | **1. Install Package:**<br>`sudo pacman -Syu docker`<br>**2. Start Service:**<br>`sudo systemctl start docker.service` | `sudo systemctl status docker.service` |
| **Fedora/RHEL** | **1. Install Package:**<br>`sudo dnf -y install docker-ce docker-ce-cli containerd.io --repo docker-ce --repo docker-ce-stable`<br>**2. Start Service:**<br>`sudo systemctl start docker` | `sudo systemctl enable docker --now` |
| **macOS** | **Install Docker Desktop (Recommended):** Download the official `.dmg` installer from the Docker website. | Start the application. CLI tools (`docker`) are installed automatically. |
| **Windows** | **Install Docker Desktop (Recommended):** Download the official installer from the Docker website.<br>**Prerequisite:** Ensure **WSL 2** is installed and enabled for optimal performance. | Start the application. Verify in PowerShell: `docker info` |
-----
### Clarification on Windows Installation
**WSL 2 and Windows:** Docker Desktop for Windows uses a lightweight Linux virtual machine (VM) powered by **WSL 2** by default. This VM hosts the Docker Engine. Therefore, while you install the *Desktop app*, you must have **WSL 2 enabled** for the container engine to function correctly and offer the best performance.
-----
## Usage Guide
### 1\. Add the Node
Find **"Docker Sandbox Runner"** in the **Advanced/Scripting** menu.
### 2\. Define Inputs
1. **Code (`code`):** Insert your Python script. The desired outputs must be assigned to variables `r1` through `r6`.
2. **Dynamic Inputs:** Right-click the node and use **"+ Add Input (Dynamic)"** to inject variables (e.g., `my_int (INT)`).
### 3\. Example Code
```python
# Access the injected variable "my_value" from the input slot
# Libraries like math, random, json, re, time are pre-loaded.
import math
if my_value > 50:
r1 = "Processed successfully."
r2 = my_value * math.pi
else:
r1 = "Value too low."
r2 = None
```
### 4\. Resource Management
The persistent container `comfyui-sandbox-worker` will run until explicitly stopped.
* **To Stop Consumption:** Stop the container manually via the Docker Desktop GUI or by CLI:
```bash
docker stop comfyui-sandbox-worker
```
-----
## License
This project is licensed under the **MIT License**.
```
MIT License
Copyright (c) 2025 [Your Name or Project Name]
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.
```
+20
View File
@@ -0,0 +1,20 @@
# __init__.py
from .sandbox_node import DockerSandboxRunner
# Mapeia a string usada no JSON do workflow para a classe Python
NODE_CLASS_MAPPINGS = {
"DockerSandboxRunner": DockerSandboxRunner
}
# Mapeia o nome da classe para o nome de exibição no menu do ComfyUI
NODE_DISPLAY_NAME_MAPPINGS = {
"DockerSandboxRunner": "Docker Sandbox Runner"
}
# ESTE CAMPO É CRÍTICO para o carregamento do JavaScript!
WEB_DIRECTORY = "./web"
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY']
# No final do arquivo __init__.py
print("✅ CUSTOM NODE DOCKER SANDBOX REGISTRADO COM SUCESSO.")
Binary file not shown.
Binary file not shown.
+348
View File
@@ -0,0 +1,348 @@
{
"id": "f32fcb48-b15c-444d-93c9-1e4ecf3d9806",
"revision": 0,
"last_node_id": 9,
"last_link_id": 7,
"nodes": [
{
"id": 6,
"type": "PreviewAny",
"pos": [
985,
215
],
"size": [
210,
76
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "source",
"type": "*",
"link": 4
}
],
"outputs": [],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.76",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {}
},
"Node name for S&R": "PreviewAny"
},
"widgets_values": [],
"color": "#4a7b7b",
"bgcolor": "#335555",
"shape": 1
},
{
"id": 8,
"type": "PreviewAny",
"pos": [
985,
499
],
"size": [
210,
165
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "source",
"type": "*",
"link": 6
}
],
"outputs": [],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.76",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {}
},
"Node name for S&R": "PreviewAny"
},
"widgets_values": [],
"color": "#4a7b7b",
"bgcolor": "#335555",
"shape": 1
},
{
"id": 4,
"type": "DockerSandboxRunner",
"pos": [
555,
215
],
"size": [
400,
256
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "code",
"type": "STRING",
"widget": {
"name": "code"
},
"link": 2
},
{
"name": "my_value (INT)",
"type": "INT",
"link": 3
}
],
"outputs": [
{
"name": "r1",
"type": "*",
"links": [
4
]
},
{
"name": "r2",
"type": "*",
"links": [
7
]
},
{
"name": "r3",
"type": "*",
"links": null
},
{
"name": "r4",
"type": "*",
"links": null
},
{
"name": "r5",
"type": "*",
"links": null
},
{
"name": "r6",
"type": "*",
"links": null
},
{
"name": "log",
"type": "STRING",
"links": [
6
]
}
],
"properties": {
"Node name for S&R": "DockerSandboxRunner",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.5.2"
}
},
"widgets_values": [
"",
10,
"512m"
],
"color": "#4a7b7b",
"bgcolor": "#335555",
"shape": 1
},
{
"id": 9,
"type": "PreviewAny",
"pos": [
985,
351
],
"size": [
210,
88
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "source",
"type": "*",
"link": 7
}
],
"outputs": [],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.76",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {}
},
"Node name for S&R": "PreviewAny"
},
"widgets_values": [],
"color": "#4a7b7b",
"bgcolor": "#335555",
"shape": 1
},
{
"id": 5,
"type": "PrimitiveInt",
"pos": [
105,
215
],
"size": [
420,
82
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "INT",
"type": "INT",
"links": [
3
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.76",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {}
},
"Node name for S&R": "PrimitiveInt"
},
"widgets_values": [
60,
"fixed"
],
"color": "#4a7b7b",
"bgcolor": "#335555",
"shape": 1
},
{
"id": 2,
"type": "PrimitiveStringMultiline",
"pos": [
105,
357
],
"size": [
420,
255
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [
2
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.76",
"Node name for S&R": "PrimitiveStringMultiline",
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {},
"version": "7.5.2"
}
},
"widgets_values": [
"# Access the injected variable \"my_value\" from the input slot\n# Libraries like math, random, json, re, time are pre-loaded.\n\nimport math \n\nif my_value > 50:\n r1 = \"Processed successfully.\"\n r2 = my_value * math.pi\nelse:\n r1 = \"Value too low.\"\n r2 = None\n\n\nprint (\"✅✅✅ Finished ✅✅✅\")"
],
"color": "#4a7b7b",
"bgcolor": "#335555",
"shape": 1
}
],
"links": [
[
2,
2,
0,
4,
0,
"STRING"
],
[
3,
5,
0,
4,
1,
"INT"
],
[
4,
4,
0,
6,
0,
"*"
],
[
6,
4,
6,
8,
0,
"*"
],
[
7,
4,
1,
9,
0,
"*"
]
],
"groups": [],
"config": {},
"extra": {
"ue_links": [],
"ds": {
"scale": 1.021848739495798,
"offset": [
103.33333333333337,
-34.98958333333326
]
},
"links_added_by_ue": [],
"frontendVersion": "1.33.10",
"VHS_latentpreview": false,
"VHS_latentpreviewrate": 0,
"VHS_MetadataImage": true,
"VHS_KeepIntermediate": true
},
"version": 0.4
}
+1
View File
@@ -0,0 +1 @@
docker
+188
View File
@@ -0,0 +1,188 @@
import docker
import json
import re
import uuid
import base64
import time
class AnyType(str):
def __ne__(self, __value: object) -> bool:
return False
def __eq__(self, __value: object) -> bool:
return True
# CLASSE RENOMEADA
class DockerSandboxRunner:
CONTAINER_NAME = "comfyui-sandbox-worker"
DOCKER_IMAGE = "python:3.9-slim"
SANDBOX_USER = "nobody"
def __init__(self):
self.client = None
try:
self.client = docker.from_env()
except Exception:
print("⚠️ AVISO: Não foi possível conectar ao Docker. Verifique se o Docker Desktop está rodando.")
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"code": ("STRING", {"multiline": True, "placeholder": "# Código limpo, focado apenas na lógica de execução."}),
# CAMPOS OCULTOS PARA CONFIGURAÇÃO TÉCNICA
"timeout": ("INT", {"default": 10, "min": 1, "max": 60, "step": 1, "hidden": True}),
"memory_limit": ("STRING", {"default": "512m", "placeholder": "Ex: 512m, 1g, 2gb", "hidden": True}),
},
"optional": {
# Campo de ação removido (como solicitado anteriormente).
}
}
RETURN_TYPES = (
AnyType("*"), AnyType("*"), AnyType("*"), AnyType("*"), AnyType("*"), AnyType("*"),
"STRING"
)
RETURN_NAMES = ("r1", "r2", "r3", "r4", "r5", "r6", "log")
FUNCTION = "execute_sandbox"
CATEGORY = "Advanced/Scripting"
def _ensure_container_running(self, mem_limit):
"""Garante que o container worker está vivo e esperando comandos"""
if not self.client:
raise RuntimeError("Docker Client não inicializado. O Docker Desktop está rodando?")
try:
container = self.client.containers.get(self.CONTAINER_NAME)
if container.status != 'running':
print(f"🛠️ Docker Sandbox: Iniciando container: {self.CONTAINER_NAME}")
container.start()
return container
except docker.errors.NotFound:
print(f"🛠️ Criando container sandbox: {self.CONTAINER_NAME} com {mem_limit} de RAM...")
return self.client.containers.run(
self.DOCKER_IMAGE,
command="tail -f /dev/null",
name=self.CONTAINER_NAME,
detach=True,
mem_limit=mem_limit,
network_mode="none",
user=self.SANDBOX_USER
)
def execute_sandbox(self, code, timeout, memory_limit="512m", **kwargs):
log_messages = []
final_results = [None] * 6
object_references = {}
variable_injection_commands = []
try:
container = self._ensure_container_running(memory_limit)
except Exception as e:
log_messages.append(f"Docker Error (Startup/Connect): {str(e)}")
return (*final_results, "\n".join(log_messages))
# 1. Prepara Variáveis (usando Literal Python)
for key, value in kwargs.items():
clean_key = key.split(" (")[0].strip().replace(" ", "_")
if value is None or isinstance(value, bool):
variable_injection_commands.append(f"{clean_key} = {str(value)}")
elif isinstance(value, (int, float, str)):
variable_injection_commands.append(f"{clean_key} = {repr(value)}")
elif isinstance(value, (list, dict)):
try:
variable_injection_commands.append(f"{clean_key} = {repr(value)}")
except:
ref_id = f"__REF__{uuid.uuid4().hex}"
object_references[ref_id] = value
variable_injection_commands.append(f"{clean_key} = '{ref_id}'")
else:
ref_id = f"__REF__{uuid.uuid4().hex}"
object_references[ref_id] = value
variable_injection_commands.append(f"{clean_key} = '{ref_id}'")
# 2. Constrói o Payload Final (Base64)
injection_script = "\n".join(variable_injection_commands)
payload_script = f"""
import json
import sys
import math, random, time, re
import signal
TIMEOUT_SECONDS = {timeout}
def signal_handler(signum, frame):
print(f"__ERROR_START__TimeoutError: Script execution exceeded {{TIMEOUT_SECONDS}} seconds.__ERROR_END__")
sys.exit(1)
signal.signal(signal.SIGALRM, signal_handler)
signal.alarm(TIMEOUT_SECONDS)
{injection_script}
_user_error = None
try:
exec({repr(code)})
finally:
signal.alarm(0)
outputs = {{}}
if _user_error:
outputs['error'] = _user_error
else:
for i in range(1, 7):
vname = f"r{{i}}"
if vname in locals():
val = locals()[vname]
try:
json.dumps(val)
outputs[vname] = val
except:
outputs[vname] = str(val)
else:
outputs[vname] = None
sys.stdout.write("__COMFY_RESULT_START__" + json.dumps(outputs) + "__COMFY_RESULT_END__")
"""
b64_payload = base64.b64encode(payload_script.encode('utf-8')).decode('utf-8')
cmd = f"python3 -c \"import base64; exec(base64.b64decode('{b64_payload}'))\""
try:
exec_result = container.exec_run(
cmd=cmd,
workdir="/tmp"
)
output_text = exec_result.output.decode('utf-8', errors='ignore')
err_match = re.search(r"__ERROR_START__(.*?)__ERROR_END__", output_text, re.DOTALL)
if err_match:
log_messages.append(f"Script Error (in Docker):\n{err_match.group(1)}")
return (*final_results, "\n".join(log_messages) + "\n" + output_text)
match = re.search(r"__COMFY_RESULT_START__(.*?)__COMFY_RESULT_END__", output_text, re.DOTALL)
if match:
json_str = match.group(1)
extracted_dict = json.loads(json_str)
if 'error' in extracted_dict:
log_messages.append(f"Runtime Error:\n{extracted_dict['error']}")
return (*final_results, "\n".join(log_messages) + "\n" + output_text)
for i in range(6):
key = f"r{i+1}"
val = extracted_dict.get(key)
if isinstance(val, str) and val in object_references:
final_results[i] = object_references[val]
else:
final_results[i] = val
log_messages.append("SUCCESS: Script executado com sucesso.")
return (*final_results, "\n".join(log_messages) + "\n" + output_text)
except Exception as e:
log_messages.append(f"Execution Infrastructure Error (Docker exec failed): {str(e)}")
return (*final_results, "\n".join(log_messages))
Vendored Executable
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+79
View File
@@ -0,0 +1,79 @@
import { app } from "/scripts/app.js";
app.registerExtension({
name: "DockerSandbox.Extensions",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
// CORRIGIDO: Checa pelo nome da classe Python (sem espaços)
if (nodeData.name === "DockerSandboxRunner") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
this.setSize([350, 150]);
return r;
};
// --- FUNÇÃO PARA INJETAR OPÇÕES DE MENU ---
const getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function (_, options) {
const me = this;
// 1. ADD INPUT
options.push({
content: "+ Add Input (Dynamic)",
callback: () => {
const name = prompt("Enter variable name (e.g., width, my_text):");
if (!name) return;
const validTypes = ["STRING", "INT", "FLOAT", "BOOLEAN", "*"];
let type = prompt(
`Enter type exactly as shown:\n\n${validTypes.join("\n")}\n\n(* = Any/Wildcard)`,
"*"
);
if (!type) return;
type = type.toUpperCase();
if (!validTypes.includes(type)) {
alert(`Invalid type! Please use one of: ${validTypes.join(", ")}`);
return;
}
const finalName = `${name} (${type})`;
const exists = me.findInputSlot(finalName);
if (exists !== -1) {
alert("An input with this name already exists.");
return;
}
me.addInput(finalName, type);
app.graph.setDirtyCanvas(true, true);
}
});
// 2. REMOVE UNUSED INPUTS
options.push({
content: "- Remove Unused Inputs",
callback: () => {
for (let i = me.inputs.length - 1; i >= 0; i--) {
const input = me.inputs[i];
// Ignora os inputs fixos (code, timeout, memory_limit)
if (["code", "timeout", "memory_limit"].includes(input.name)) continue;
// Remove se não estiver conectado
if (!input.link) {
me.removeInput(i);
}
}
app.graph.setDirtyCanvas(true, true);
}
});
if (getExtraMenuOptions) {
return getExtraMenuOptions.apply(this, arguments);
}
};
}
}
});