ComfyUI/comfy/isolation/proxies/helper_proxies.py
John Pollock c5e7b9cdaf feat(isolation): process isolation for custom nodes via pyisolate
Adds opt-in process isolation for custom nodes using pyisolate's
bwrap sandbox and JSON-RPC bridge. Each isolated node pack runs in
its own child process with zero-copy tensor transfer via shared memory.

Core infrastructure:
- CLI flag --use-process-isolation to enable isolation
- Host/child startup fencing via PYISOLATE_CHILD env var
- Manifest-driven node discovery and extension loading
- JSON-RPC bridge between host and child processes
- Shared memory forensics for leak detection

Proxy layer:
- ModelPatcher, CLIP, VAE, and ModelSampling proxies
- Host service proxies (folder_paths, model_management, progress, etc.)
- Proxy base with automatic method forwarding

Execution integration:
- Extension wrapper with V3 hidden param mapping
- Runtime helpers for isolated node execution
- Host policy for node isolation decisions
- Fenced sampler device handling and model ejection parity

Serializers for cross-process data transfer:
- File3D (GLB), PLY (structured + gaussian), NPZ (streaming frames),
  VIDEO (VideoFromFile + VideoFromComponents) serializers
- data_type flag in SerializerRegistry for type-aware dispatch
- Isolated get_temp_directory() fence

New core save nodes:
- SavePLY and SaveNPZ with comfytype registrations (Ply, Npz)

DynamicVRAM compatibility:
- comfy-aimdo early init gated by isolation fence

Tests:
- Integration and policy tests for isolation lifecycle
- Manifest loader, host policy, proxy, and adapter unit tests

Depends on: pyisolate >= 0.9.2
2026-03-12 01:13:43 -05:00

99 lines
3.2 KiB
Python

from __future__ import annotations
from typing import Any, Dict, Optional
class AnyTypeProxy(str):
"""Replacement for custom AnyType objects used by some nodes."""
def __new__(cls, value: str = "*"):
return super().__new__(cls, value)
def __ne__(self, other): # type: ignore[override]
return False
class FlexibleOptionalInputProxy(dict):
"""Replacement for FlexibleOptionalInputType to allow dynamic inputs."""
def __init__(self, flex_type, data: Optional[Dict[str, object]] = None):
super().__init__()
self.type = flex_type
if data:
self.update(data)
def __getitem__(self, key): # type: ignore[override]
return (self.type,)
def __contains__(self, key): # type: ignore[override]
return True
class ByPassTypeTupleProxy(tuple):
"""Replacement for ByPassTypeTuple to mirror wildcard fallback behavior."""
def __new__(cls, values):
return super().__new__(cls, values)
def __getitem__(self, index): # type: ignore[override]
if index >= len(self):
return AnyTypeProxy("*")
return super().__getitem__(index)
def _restore_special_value(value: Any) -> Any:
if isinstance(value, dict):
if value.get("__pyisolate_any_type__"):
return AnyTypeProxy(value.get("value", "*"))
if value.get("__pyisolate_flexible_optional__"):
flex_type = _restore_special_value(value.get("type"))
data_raw = value.get("data")
data = (
{k: _restore_special_value(v) for k, v in data_raw.items()}
if isinstance(data_raw, dict)
else {}
)
return FlexibleOptionalInputProxy(flex_type, data)
if value.get("__pyisolate_tuple__") is not None:
return tuple(
_restore_special_value(v) for v in value["__pyisolate_tuple__"]
)
if value.get("__pyisolate_bypass_tuple__") is not None:
return ByPassTypeTupleProxy(
tuple(
_restore_special_value(v)
for v in value["__pyisolate_bypass_tuple__"]
)
)
return {k: _restore_special_value(v) for k, v in value.items()}
if isinstance(value, list):
return [_restore_special_value(v) for v in value]
return value
def restore_input_types(raw: Dict[str, object]) -> Dict[str, object]:
"""Restore serialized INPUT_TYPES payload back into ComfyUI-compatible objects."""
if not isinstance(raw, dict):
return raw # type: ignore[return-value]
restored: Dict[str, object] = {}
for section, entries in raw.items():
if isinstance(entries, dict) and entries.get("__pyisolate_flexible_optional__"):
restored[section] = _restore_special_value(entries)
elif isinstance(entries, dict):
restored[section] = {
k: _restore_special_value(v) for k, v in entries.items()
}
else:
restored[section] = _restore_special_value(entries)
return restored
__all__ = [
"AnyTypeProxy",
"FlexibleOptionalInputProxy",
"ByPassTypeTupleProxy",
"restore_input_types",
]