diff --git a/.env.example b/.env.example
index c8c4af9b3d537..4ef7837fab756 100644
--- a/.env.example
+++ b/.env.example
@@ -119,10 +119,10 @@
# =============================================================================
# TERMINAL TOOL CONFIGURATION
# =============================================================================
-# Backend type: "local", "singularity", "docker", "modal", or "ssh"
+# Backend type: "local", "singularity", "docker", "podman", "modal", or "ssh"
# Terminal backend is configured in ~/.hermes/config.yaml (terminal.backend).
# Use 'hermes setup' or 'hermes config set terminal.backend docker' to change.
-# Supported: local, docker, singularity, modal, ssh
+# Supported: local, docker, podman, singularity, modal, ssh
#
# Only override here if you need to force a backend without touching config.yaml:
# TERMINAL_ENV=local
@@ -135,9 +135,9 @@ TERMINAL_MODAL_IMAGE=nikolaik/python-nodejs:python3.11-nodejs20
# Working directory for terminal commands
# For local backend: "." means current directory (resolved automatically)
-# For remote backends (ssh/docker/modal/singularity): use an absolute path
+# For remote backends (ssh/docker/podman/modal/singularity): use an absolute path
# INSIDE the target environment, or leave unset for the backend's default
-# (/root for modal, / for docker, ~ for ssh). Do NOT use a host-local path.
+# (/root for modal, / for docker/podman, ~ for ssh). Do NOT use a host-local path.
# Usually managed by config.yaml (terminal.cwd) — uncomment to override
# TERMINAL_CWD=.
@@ -168,7 +168,7 @@ TERMINAL_LIFETIME_SECONDS=300
# SUDO SUPPORT (works with ALL terminal backends)
# =============================================================================
# If set, enables sudo commands by piping password via `sudo -S`.
-# Works with: local, docker, singularity, modal, and ssh backends.
+# Works with: local, docker, podman, singularity, modal, and ssh backends.
#
# SECURITY WARNING: Password stored in plaintext. Only use on trusted machines.
#
diff --git a/AGENTS.md b/AGENTS.md
index 8045c3d213df6..0b25bd16546f8 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -51,7 +51,7 @@ hermes-agent/
│ ├── code_execution_tool.py # execute_code sandbox
│ ├── delegate_tool.py # Subagent delegation
│ ├── mcp_tool.py # MCP client (~1050 lines)
-│ └── environments/ # Terminal backends (local, docker, ssh, modal, daytona, singularity)
+│ └── environments/ # Terminal backends (local, docker, podman, ssh, modal, daytona, singularity)
├── gateway/ # Messaging platform gateway
│ ├── run.py # Main loop, slash commands, message dispatch
│ ├── session.py # SessionStore — conversation persistence
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4577454e441ca..05c25dd06df5c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -157,7 +157,7 @@ hermes-agent/
│ ├── skill_tools.py # Skill search, load, manage
│ └── environments/ # Terminal execution backends
│ ├── base.py # BaseEnvironment ABC
-│ ├── local.py, docker.py, ssh.py, singularity.py, modal.py, daytona.py
+│ ├── local.py, docker.py, podman.py, ssh.py, singularity.py, modal.py, daytona.py
│
├── gateway/ # Messaging gateway
│ ├── run.py # GatewayRunner — platform lifecycle, message routing, cron
diff --git a/agent/skill_commands.py b/agent/skill_commands.py
index 18414199dceb4..a30337a995b46 100644
--- a/agent/skill_commands.py
+++ b/agent/skill_commands.py
@@ -29,7 +29,7 @@ def build_plan_path(
"""Return the default workspace-relative markdown path for a /plan invocation.
Relative paths are intentional: file tools are task/backend-aware and resolve
- them against the active working directory for local, docker, ssh, modal,
+ them against the active working directory for local, docker, ssh, modal, podman,
daytona, and similar terminal backends. That keeps the plan with the active
workspace instead of the Hermes host's global home directory.
"""
diff --git a/agent/smart_model_routing.py b/agent/smart_model_routing.py
index 6d482be270516..afd292828ee4e 100644
--- a/agent/smart_model_routing.py
+++ b/agent/smart_model_routing.py
@@ -42,6 +42,7 @@
"subagent",
"cron",
"docker",
+ "podman",
"kubernetes",
}
diff --git a/batch_runner.py b/batch_runner.py
index 195452c0ae0ce..c8646b67c4a0a 100644
--- a/batch_runner.py
+++ b/batch_runner.py
@@ -288,6 +288,44 @@ def _process_single_prompt(
except Exception as img_err:
if config.get("verbose"):
print(f" Prompt {prompt_index}: Docker image check failed: {img_err}", flush=True)
+ elif env_type == "podman":
+ import subprocess as _sp
+ try:
+ cmd = ["podman", "image", "inspect", container_image]
+ if config.get("podman_rootful"):
+ cmd = ["sudo"] + cmd
+
+ probe = _sp.run(
+ cmd,
+ capture_output=True, timeout=10,
+ )
+ if probe.returncode != 0:
+ if config.get("verbose"):
+ print(f" Prompt {prompt_index}: Pulling podman image {container_image}...", flush=True)
+
+ cmd = ["podman", "pull", container_image]
+ if config.get("podman_rootful"):
+ cmd = ["sudo"] + cmd
+
+ pull = _sp.run(
+ cmd,
+ capture_output=True, text=True, timeout=600,
+ )
+ if pull.returncode != 0:
+ return {
+ "success": False,
+ "prompt_index": prompt_index,
+ "error": f"Podman image not available: {container_image}\n{pull.stderr[:500]}",
+ "trajectory": None,
+ "tool_stats": {},
+ "toolsets_used": [],
+ "metadata": {"batch_num": batch_num, "timestamp": datetime.now().isoformat()},
+ }
+ except FileNotFoundError:
+ pass # Docker CLI not installed — skip check (e.g., Modal backend)
+ except Exception as img_err:
+ if config.get("verbose"):
+ print(f" Prompt {prompt_index}: Podman image check failed: {img_err}", flush=True)
from tools.terminal_tool import register_task_env_overrides
overrides = {
@@ -295,6 +333,7 @@ def _process_single_prompt(
"modal_image": container_image,
"singularity_image": f"docker://{container_image}",
"daytona_image": container_image,
+ "podman_image": container_image,
}
if prompt_data.get("cwd"):
overrides["cwd"] = prompt_data["cwd"]
diff --git a/cli-config.yaml.example b/cli-config.yaml.example
index 5807cef7aa586..7b01d2b2181c8 100644
--- a/cli-config.yaml.example
+++ b/cli-config.yaml.example
@@ -211,8 +211,33 @@ terminal:
# daytona_image: "nikolaik/python-nodejs:python3.11-nodejs20"
# container_disk: 10240 # Daytona max is 10GB per sandbox
+# -----------------------------------------------------------------------------
+# OPTION 7: Podman container
+# Commands run in an isolated Podman container
+# Great for: reproducible environments, testing, isolation
+# -----------------------------------------------------------------------------
+# terminal:
+# backend: "podman"
+# cwd: "/workspace" # Path INSIDE the container (default: /)
+# timeout: 180
+# lifetime_seconds: 300
+# docker_image: "docker.io/nikolaik/python-nodejs:python3.11-nodejs20"
+# docker_mount_cwd_to_workspace: true # Explicit opt-in: mount your launch cwd into /workspace
+# # Optional: explicitly forward selected env vars into Docker.
+# # These values come from your current shell first, then ~/.hermes/.env.
+# # Warning: anything forwarded here is visible to commands run in the container.
+# docker_forward_env:
+# - "GITHUB_TOKEN"
+# - "NPM_TOKEN"
+# podman_user: pn # default: unset - i.e. whatever is the default user of the container image
+# podman_userns: keep-id # default: host
+# podman_privileged: true # default: false - i.e. no `--privileged` command line option
+# podman_extra_capabilities: [] # default: []
+# podman_extra_args: [] # default: []
+# podman_rootful: true # default: false - i.e. run Podman as just `podman` instead of `sudo podman`
+
#
-# --- Container resource limits (docker, singularity, modal, daytona -- ignored for local/ssh) ---
+# --- Container resource limits (docker, podman, singularity, modal, daytona -- ignored for local/ssh) ---
# These settings apply to all container backends. They control the resources
# allocated to the sandbox and whether its filesystem persists across sessions.
container_cpu: 1 # CPU cores
diff --git a/cli.py b/cli.py
index 223d360932d0e..6e4897fe292b7 100644
--- a/cli.py
+++ b/cli.py
@@ -223,12 +223,18 @@ def load_cli_config() -> Dict[str, Any]:
"timeout": 60,
"lifetime_seconds": 300,
"docker_image": "nikolaik/python-nodejs:python3.11-nodejs20",
+ "podman_image": "docker.io/nikolaik/python-nodejs:python3.11-nodejs20",
"docker_forward_env": [],
"singularity_image": "docker://nikolaik/python-nodejs:python3.11-nodejs20",
"modal_image": "nikolaik/python-nodejs:python3.11-nodejs20",
"daytona_image": "nikolaik/python-nodejs:python3.11-nodejs20",
"docker_volumes": [], # host:container volume mounts for Docker backend
"docker_mount_cwd_to_workspace": False, # explicit opt-in only; default off for sandbox isolation
+ "podman_userns": "host",
+ "podman_extra_args": [],
+ "podman_extra_capabilities": [],
+ "podman_privileged": False,
+ "podman_rootful": False,
},
"browser": {
"inactivity_timeout": 120, # Auto-cleanup inactive browser sessions after 2 min
@@ -417,6 +423,7 @@ def load_cli_config() -> Dict[str, Any]:
"timeout": "TERMINAL_TIMEOUT",
"lifetime_seconds": "TERMINAL_LIFETIME_SECONDS",
"docker_image": "TERMINAL_DOCKER_IMAGE",
+ "podman_image": "TERMINAL_PODMAN_IMAGE",
"docker_forward_env": "TERMINAL_DOCKER_FORWARD_ENV",
"singularity_image": "TERMINAL_SINGULARITY_IMAGE",
"modal_image": "TERMINAL_MODAL_IMAGE",
diff --git a/environments/README.md b/environments/README.md
index 9677fdb70ef65..76b9474a3741a 100644
--- a/environments/README.md
+++ b/environments/README.md
@@ -40,7 +40,7 @@ This directory contains the integration layer between **hermes-agent's** tool-ca
- `evaluate_log()` for saving eval results to JSON + samples.jsonl
**HermesAgentBaseEnv** (`hermes_base_env.py`) extends BaseEnv with hermes-agent specifics:
-- Sets `os.environ["TERMINAL_ENV"]` to configure the terminal backend (local, docker, modal, daytona, ssh, singularity)
+- Sets `os.environ["TERMINAL_ENV"]` to configure the terminal backend (local, docker, podman, modal, daytona, ssh, singularity)
- Resolves hermes-agent toolsets via `_resolve_tools_for_group()` (calls `get_tool_definitions()` which queries `tools/registry.py`)
- Implements `collect_trajectory()` which runs the full agent loop and computes rewards
- Supports two-phase operation (Phase 1: OpenAI server, Phase 2: VLLM ManagedServer)
@@ -318,7 +318,7 @@ For eval benchmarks, follow the pattern in `terminalbench2_env.py`:
| `distribution` | Probabilistic toolset distribution name | `None` |
| `max_agent_turns` | Max LLM calls per rollout | `30` |
| `agent_temperature` | Sampling temperature | `1.0` |
-| `terminal_backend` | `local`, `docker`, `modal`, `daytona`, `ssh`, `singularity` | `local` |
+| `terminal_backend` | `local`, `docker`, `podman`, `modal`, `daytona`, `ssh`, `singularity` | `local` |
| `system_prompt` | System message for the agent | `None` |
| `tool_call_parser` | Parser name for Phase 2 | `hermes` |
| `eval_handling` | `STOP_TRAIN`, `LIMIT_TRAIN`, `NONE` | `STOP_TRAIN` |
diff --git a/environments/agent_loop.py b/environments/agent_loop.py
index 891ce42f4481d..ccc28583956d4 100644
--- a/environments/agent_loop.py
+++ b/environments/agent_loop.py
@@ -401,7 +401,7 @@ def _tc_to_dict(tc):
tool_elapsed = _time.monotonic() - tool_submit_time
else:
# Run tool calls in a thread pool so backends that
- # use asyncio.run() internally (modal, docker, daytona) get
+ # use asyncio.run() internally (modal, docker, podman, daytona) get
# a clean event loop instead of deadlocking.
loop = asyncio.get_event_loop()
# Capture current tool_name/args for the lambda
diff --git a/environments/hermes_base_env.py b/environments/hermes_base_env.py
index ededab355f06b..0aaed91f6348d 100644
--- a/environments/hermes_base_env.py
+++ b/environments/hermes_base_env.py
@@ -119,7 +119,7 @@ class HermesAgentEnvConfig(BaseEnvConfig):
# --- Terminal backend ---
terminal_backend: str = Field(
default="local",
- description="Terminal backend: 'local', 'docker', 'modal', 'daytona', 'ssh', 'singularity'. "
+ description="Terminal backend: 'local', 'docker', 'podman', 'modal', 'daytona', 'ssh', 'singularity'. "
"Modal or Daytona recommended for production RL (cloud isolation per rollout).",
)
terminal_timeout: int = Field(
diff --git a/environments/tool_context.py b/environments/tool_context.py
index 10f537d72432d..68125b4d575e6 100644
--- a/environments/tool_context.py
+++ b/environments/tool_context.py
@@ -44,7 +44,7 @@ async def compute_reward(self, item, result, ctx):
def _run_tool_in_thread(tool_name: str, arguments: Dict[str, Any], task_id: str) -> str:
"""
Run a tool call in a thread pool executor so backends that use asyncio.run()
- internally (modal, docker, daytona) get a clean event loop.
+ internally (modal, docker, podman, daytona) get a clean event loop.
If we're already in an async context, executes handle_function_call() in a
disposable worker thread and blocks for the result.
@@ -95,7 +95,7 @@ def terminal(self, command: str, timeout: int = 180) -> Dict[str, Any]:
backend = os.getenv("TERMINAL_ENV", "local")
logger.debug("ToolContext.terminal [%s backend] task=%s: %s", backend, self.task_id[:8], command[:100])
- # Run via thread helper so modal/docker/daytona backends' asyncio.run() doesn't deadlock
+ # Run via thread helper so modal/docker/podman/daytona backends' asyncio.run() doesn't deadlock
result = _run_tool_in_thread(
"terminal",
{"command": command, "timeout": timeout},
diff --git a/gateway/run.py b/gateway/run.py
index bf5103d126953..c020ea6863b29 100644
--- a/gateway/run.py
+++ b/gateway/run.py
@@ -111,6 +111,7 @@ def _ensure_ssl_certs() -> None:
"timeout": "TERMINAL_TIMEOUT",
"lifetime_seconds": "TERMINAL_LIFETIME_SECONDS",
"docker_image": "TERMINAL_DOCKER_IMAGE",
+ "podman_image": "TERMINAL_PODMAN_IMAGE",
"docker_forward_env": "TERMINAL_DOCKER_FORWARD_ENV",
"singularity_image": "TERMINAL_SINGULARITY_IMAGE",
"modal_image": "TERMINAL_MODAL_IMAGE",
@@ -126,6 +127,12 @@ def _ensure_ssl_certs() -> None:
"docker_volumes": "TERMINAL_DOCKER_VOLUMES",
"sandbox_dir": "TERMINAL_SANDBOX_DIR",
"persistent_shell": "TERMINAL_PERSISTENT_SHELL",
+ "podman_user": "TERMINAL_PODMAN_USER",
+ "podman_userns": "TERMINAL_PODMAN_USERNS",
+ "podman_privileged": "TERMINAL_PODMAN_PRIVILEGED",
+ "podman_extra_capabilities": "TERMINAL_PODMAN_EXTRA_CAPABILITIES",
+ "podman_extra_args": "TERMINAL_PODMAN_EXTRA_ARGS",
+ "podman_rootful": "TERMINAL_PODMAN_ROOTFUL",
}
for _cfg_key, _env_var in _terminal_env_map.items():
if _cfg_key in _terminal_cfg:
diff --git a/hermes_cli/config.py b/hermes_cli/config.py
index 2cb6a8d62a5a5..adf6f1d245d5c 100644
--- a/hermes_cli/config.py
+++ b/hermes_cli/config.py
@@ -297,6 +297,7 @@ def _ensure_hermes_home_managed(home: Path):
# are passed through automatically; this list is for non-skill use cases.
"env_passthrough": [],
"docker_image": "nikolaik/python-nodejs:python3.11-nodejs20",
+ "podman_image": "docker.io/nikolaik/python-nodejs:python3.11-nodejs20",
"docker_forward_env": [],
# Explicit environment variables to set inside Docker containers.
# Unlike docker_forward_env (which reads values from the host process),
@@ -324,6 +325,13 @@ def _ensure_hermes_home_managed(home: Path):
# Enabled by default for non-local backends (SSH); local is always opt-in
# via TERMINAL_LOCAL_PERSISTENT env var.
"persistent_shell": True,
+ # Podman-specific options
+ "podman_userns": "host", # --userns flag (e.g., "keep-id", "auto")
+ "podman_user": "", # --user flag (e.g., "1000:1000", "nonroot")
+ "podman_privileged": False, # --privileged flag
+ "podman_extra_capabilities": [], # Additional --cap-add values (additive to defaults)
+ "podman_extra_args": [], # Arbitrary additional podman run flags
+ "podman_rootful": False, # Run podman commands with sudo (i.e. rootful mode)
},
"browser": {
@@ -1278,6 +1286,48 @@ def _ensure_hermes_home_managed(home: Path):
"password": True,
"category": "messaging",
},
+ "TERMINAL_PODMAN_USERNS": {
+ "description": "Podman user namespace mode (--userns flag)",
+ "prompt": "Podman User Namespace",
+ "url": "https://docs.podman.io/en/latest/markdown/podman-run.1.html#userns-mode",
+ "password": False,
+ "category": "tool",
+ },
+ "TERMINAL_PODMAN_USER": {
+ "description": "Podman user to run as inside container (--user flag)",
+ "prompt": "Podman User",
+ "url": "https://docs.podman.io/en/latest/markdown/podman-run.1.html#user-user",
+ "password": False,
+ "category": "tool",
+ },
+ "TERMINAL_PODMAN_PRIVILEGED": {
+ "description": "Run Podman containers in privileged mode (--privileged flag)",
+ "prompt": "Podman Privileged Mode",
+ "url": "https://docs.podman.io/en/latest/markdown/podman-run.1.html#privileged",
+ "password": False,
+ "category": "tool",
+ },
+ "TERMINAL_PODMAN_EXTRA_CAPABILITIES": {
+ "description": "Additional Linux capabilities to add to Podman containers (--cap-add)",
+ "prompt": "Podman Extra Capabilities",
+ "url": "https://docs.podman.io/en/latest/markdown/podman-run.1.html#cap-add-capability",
+ "password": False,
+ "category": "tool",
+ },
+ "TERMINAL_PODMAN_EXTRA_ARGS": {
+ "description": "Additional arbitrary arguments for podman run (JSON array)",
+ "prompt": "Podman Extra Arguments",
+ "url": "https://docs.podman.io/en/latest/markdown/podman-run.1.html",
+ "password": False,
+ "category": "tool",
+ },
+ "TERMINAL_PODMAN_ROOTFUL": {
+ "description": "Run Podman commands with sudo",
+ "prompt": "Podman Use Sudo",
+ "url": "",
+ "password": False,
+ "category": "tool",
+ },
# ── Agent settings ──
"MESSAGING_CWD": {
@@ -2584,6 +2634,8 @@ def show_config():
if terminal.get('backend') == 'docker':
print(f" Docker image: {terminal.get('docker_image', 'nikolaik/python-nodejs:python3.11-nodejs20')}")
+ elif terminal.get('backend') == 'podman':
+ print(f" Podman image: {terminal.get('podman_image', 'docker.io/nikolaik/python-nodejs:python3.11-nodejs20')}")
elif terminal.get('backend') == 'singularity':
print(f" Image: {terminal.get('singularity_image', 'docker://nikolaik/python-nodejs:python3.11-nodejs20')}")
elif terminal.get('backend') == 'modal':
diff --git a/hermes_cli/doctor.py b/hermes_cli/doctor.py
index 1a2f839c0b32c..2b2a77c0009f9 100644
--- a/hermes_cli/doctor.py
+++ b/hermes_cli/doctor.py
@@ -557,6 +557,20 @@ def run_doctor(args):
else:
check_warn("docker not found", "(optional)")
+ # Podman (optional)
+ if terminal_env == "podman":
+ if not shutil.which("podman"):
+ check_fail("podman not found", "(required for TERMINAL_ENV=podman)")
+ issues.append("Install Podman or change TERMINAL_ENV")
+ else:
+ if shutil.which("podman"):
+ check_ok("podman", "(optional)")
+ else:
+ if _is_termux():
+ check_info("Podman backend is not available inside Termux (expected on Android)")
+ else:
+ check_warn("podman not found", "(optional)")
+
# SSH (if using ssh backend)
if terminal_env == "ssh":
ssh_host = os.getenv("TERMINAL_SSH_HOST")
diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py
index 216ab54a587e5..cc87cc6a8b07e 100644
--- a/hermes_cli/setup.py
+++ b/hermes_cli/setup.py
@@ -11,6 +11,7 @@
Config files are stored in ~/.hermes/ for easy access.
"""
+import json
import importlib.util
import logging
import os
@@ -1185,10 +1186,10 @@ def setup_terminal_backend(config: dict):
"SSH - run on a remote machine",
"Daytona - persistent cloud development environment",
]
- idx_to_backend = {0: "local", 1: "docker", 2: "modal", 3: "ssh", 4: "daytona"}
- backend_to_idx = {"local": 0, "docker": 1, "modal": 2, "ssh": 3, "daytona": 4}
+ idx_to_backend = {0: "local", 1: "docker", 2: "modal", 3: "ssh", 4: "daytona", 5: "podman"}
+ backend_to_idx = {"local": 0, "docker": 1, "modal": 2, "ssh": 3, "daytona": 4, "podman": 5}
- next_idx = 5
+ next_idx = 6
if is_linux:
terminal_choices.append("Singularity/Apptainer - HPC-friendly container")
idx_to_backend[next_idx] = "singularity"
@@ -1263,6 +1264,79 @@ def setup_terminal_backend(config: dict):
_prompt_container_resources(config)
+ elif selected_backend == "podman":
+ print_success("Terminal backend: Podman")
+
+ # Check if Docker is available
+ docker_bin = shutil.which("podman")
+ if not docker_bin:
+ print_warning("Podman not found in PATH!")
+ print_info("Install Podman: https://podman.io/docs/installation")
+ else:
+ print_info(f"Podman found: {docker_bin}")
+
+ # Podman image
+ current_image = config.get("terminal", {}).get(
+ "podman_image", "docker.io/nikolaik/python-nodejs:python3.11-nodejs20"
+ )
+ image = prompt(" Podman image", current_image)
+ config["terminal"]["podman_image"] = image
+ save_env_value("TERMINAL_PODMAN_IMAGE", image)
+
+ terminal = config.get("terminal")
+ # User namespace mapping
+ current_userns = terminal.get("podman_userns", "host")
+ terminal["podman_userns"] = prompt(" Podman user namespace mapping", str(current_userns))
+
+ # User
+ current_user = terminal.get("podman_user", "")
+ user = prompt(" Podman user", str(current_user))
+
+ # Extra capabilities
+ current_extra_caps = terminal.get("podman_extra_capabilities", [])
+ extra_caps = prompt(" Podman extra capabilities (space-sparated)", " ".join(current_extra_caps))
+ terminal["podman_extra_capabilities"] = extra_caps.split(" ").filter(lambda x: len(x) > 0)
+
+ # Privileged mode
+ current_privileged = "Yes" if (str(terminal.get("podman_privileged", False)).lower() in ("true", "yes", "1")) else "No"
+ idx_to_privileged = {
+ 0: "no",
+ 1: "yes",
+ }
+ privileged_to_idx = {
+ "yes": 1,
+ "no": 0,
+ }
+ privileged_choices = ["No", "Yes", f"Keep current ({current_privileged})"]
+ privileged_idx = prompt_choice("Podman privileged mode (--privileged)", privileged_choices)
+ if privileged_idx != 2:
+ terminal["podman_privileged"] = (privileged_idx == 1)
+
+ # Extra args
+ current_extra_args = terminal.get("podman_extra_args", [])
+ extra_args = prompt(" Podman extra args (array of strings as JSON text)", json.dumps(current_extra_args))
+ try:
+ terminal["podman_extra_args"] = json.loads(extra_args)
+ except ValueError:
+ pass
+
+ # Rootful
+ current_rootful = "Yes" if (str(terminal.get("podman_rootful", False)).lower() in ("true", "yes", "1")) else "No"
+ idx_to_rootful = {
+ 0: "no",
+ 1: "yes",
+ }
+ rootful_to_idx = {
+ "no": 0,
+ "yes": 1,
+ }
+ rootful_choices = ["No", "Yes", f"Keep current ({current_rootful})"]
+ rootful_idx = prompt_choice("Podman rootful mode (`sudo podman`)", rootful_choices)
+ if rootful_idx != 2:
+ terminal["podman_rootful"] = (rootful_idx == 1)
+
+ _prompt_container_resources(config)
+
elif selected_backend == "singularity":
print_success("Terminal backend: Singularity/Apptainer")
diff --git a/hermes_cli/status.py b/hermes_cli/status.py
index baba4f359d587..837ef33d987a0 100644
--- a/hermes_cli/status.py
+++ b/hermes_cli/status.py
@@ -281,6 +281,9 @@ def show_status(args):
elif terminal_env == "docker":
docker_image = os.getenv("TERMINAL_DOCKER_IMAGE", "python:3.11-slim")
print(f" Docker Image: {docker_image}")
+ elif terminal_env == "podman":
+ podman_image = os.getenv("TERMINAL_PODMAN_IMAGE", "docker.io/nikolaik/python-nodejs:python3.11-nodejs20")
+ print(f" Podman Image: {podman_image}")
elif terminal_env == "daytona":
daytona_image = os.getenv("TERMINAL_DAYTONA_IMAGE", "nikolaik/python-nodejs:python3.11-nodejs20")
print(f" Daytona Image: {daytona_image}")
diff --git a/package-lock.json b/package-lock.json
index 1e54db9aa55d8..646e7c1440687 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
+ "@askjo/camoufox-browser": "^1.0.0",
"agent-browser": "^0.13.0"
},
"engines": {
@@ -38,6 +39,26 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
+ "node_modules/@askjo/camoufox-browser": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/@askjo/camoufox-browser/-/camoufox-browser-1.0.12.tgz",
+ "integrity": "sha512-MxRvjK6SkX6zJSNleoO32g9iwhJAcXpaAgj4pik7y2SrYXqcHllpG7FfLkKE7d5bnBt7pO82rdarVYu6xtW2RA==",
+ "deprecated": "Renamed to @askjo/camofox-browser",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "camoufox-js": "^0.8.5",
+ "dotenv": "^17.2.3",
+ "express": "^4.18.2",
+ "playwright": "^1.50.0",
+ "playwright-core": "^1.58.0",
+ "playwright-extra": "^4.3.6",
+ "puppeteer-extra-plugin-stealth": "^2.11.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -105,12 +126,39 @@
"node": ">=18"
}
},
+ "node_modules/@sindresorhus/is": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
+ "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/is?sponsor=1"
+ }
+ },
"node_modules/@tootallnate/quickjs-emscripten": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
"integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
"license": "MIT"
},
+ "node_modules/@types/debug": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
+ "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/ms": "*"
+ }
+ },
+ "node_modules/@types/ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+ "license": "MIT"
+ },
"node_modules/@types/node": {
"version": "20.19.33",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz",
@@ -263,6 +311,28 @@
"node": ">=6.5"
}
},
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/adm-zip": {
+ "version": "0.5.17",
+ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz",
+ "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0"
+ }
+ },
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
@@ -358,6 +428,21 @@
"node": ">= 0.4"
}
},
+ "node_modules/arr-union": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
+ "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
"node_modules/ast-types": {
"version": "0.13.4",
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
@@ -522,6 +607,18 @@
],
"license": "MIT"
},
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.16",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz",
+ "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==",
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/basic-ftp": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz",
@@ -531,12 +628,135 @@
"node": ">=10.0.0"
}
},
+ "node_modules/better-sqlite3": {
+ "version": "12.8.0",
+ "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz",
+ "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "bindings": "^1.5.0",
+ "prebuild-install": "^7.1.1"
+ },
+ "engines": {
+ "node": "20.x || 22.x || 23.x || 24.x || 25.x"
+ }
+ },
+ "node_modules/bindings": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+ "license": "MIT",
+ "dependencies": {
+ "file-uri-to-path": "1.0.0"
+ }
+ },
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/bl/node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/bl/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
"license": "MIT"
},
+ "node_modules/body-parser": {
+ "version": "1.20.4",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
+ "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.14.0",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/body-parser/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/body-parser/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/body-parser/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -552,6 +772,39 @@
"balanced-match": "^1.0.0"
}
},
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
@@ -591,6 +844,188 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"license": "MIT"
},
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camoufox-js": {
+ "version": "0.8.5",
+ "resolved": "https://registry.npmjs.org/camoufox-js/-/camoufox-js-0.8.5.tgz",
+ "integrity": "sha512-20ihPbspAcOVSUTX9Drxxp0C116DON1n8OVA1eUDglWZiHwiHwFVFOMrIEBwAHMZpU11mIEH/kawJtstRIrDPA==",
+ "license": "MPL-2.0",
+ "dependencies": {
+ "adm-zip": "^0.5.16",
+ "better-sqlite3": "^12.2.0",
+ "commander": "^14.0.0",
+ "fingerprint-generator": "^2.1.66",
+ "glob": "^13.0.0",
+ "impit": "^0.7.0",
+ "language-tags": "^2.0.1",
+ "maxmind": "^5.0.0",
+ "progress": "^2.0.3",
+ "ua-parser-js": "^2.0.2",
+ "xml2js": "^0.6.2"
+ },
+ "bin": {
+ "camoufox-js": "dist/__main__.js"
+ },
+ "engines": {
+ "node": ">= 20"
+ },
+ "peerDependencies": {
+ "playwright-core": "*"
+ }
+ },
+ "node_modules/camoufox-js/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/camoufox-js/node_modules/brace-expansion": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
+ "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/camoufox-js/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/camoufox-js/node_modules/glob": {
+ "version": "13.0.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
+ "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "minimatch": "^10.2.2",
+ "minipass": "^7.1.3",
+ "path-scurry": "^2.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/camoufox-js/node_modules/lru-cache": {
+ "version": "11.3.2",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.2.tgz",
+ "integrity": "sha512-wgWa6FWQ3QRRJbIjbsldRJZxdxYngT/dO0I5Ynmlnin8qy7tC6xYzbcJjtN4wHLXtkbVwHzk0C+OejVw1XM+DQ==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/camoufox-js/node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/camoufox-js/node_modules/path-scurry": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
+ "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001787",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz",
+ "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
"node_modules/chalk": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
@@ -645,6 +1080,12 @@
"url": "https://github.com/sponsors/fb55"
}
},
+ "node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+ "license": "ISC"
+ },
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -732,6 +1173,22 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
+ "node_modules/clone-deep": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz",
+ "integrity": "sha512-we+NuQo2DHhSl+DP6jlUiAhyAjBQrYnpOk15rN6c6JSPScjiCLh8IbSU+VTcph6YS3o7mASE8a0+gbZ7ChLpgg==",
+ "license": "MIT",
+ "dependencies": {
+ "for-own": "^0.1.3",
+ "is-plain-object": "^2.0.1",
+ "kind-of": "^3.0.2",
+ "lazy-cache": "^1.0.3",
+ "shallow-clone": "^0.1.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -775,12 +1232,54 @@
"node": ">= 14"
}
},
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "license": "MIT"
+ },
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
"license": "ISC"
},
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "license": "MIT"
+ },
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@@ -924,9 +1423,42 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/deepmerge-ts": {
- "version": "7.1.5",
- "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/deepmerge": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/deepmerge-ts": {
+ "version": "7.1.5",
+ "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
"integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==",
"license": "BSD-3-Clause",
"engines": {
@@ -947,6 +1479,54 @@
"node": ">= 14"
}
},
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/detect-europe-js": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/detect-europe-js/-/detect-europe-js-0.1.2.tgz",
+ "integrity": "sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/faisalman"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ua-parser-js"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/faisalman"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
@@ -1002,6 +1582,47 @@
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
+ "node_modules/dot-prop": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz",
+ "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-obj": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "17.4.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.1.tgz",
+ "integrity": "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -1092,12 +1713,33 @@
"node": "^20.17.0 || >=22.9.0"
}
},
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.333",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.333.tgz",
+ "integrity": "sha512-skNh4FsE+IpCJV7xAQGbQ4eyOGvcEctVBAk7a5KPzxC3alES9rLrT+2IsPRPgeQr8LVxdJr8BHQ9481+TOr0xg==",
+ "license": "ISC"
+ },
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"license": "MIT"
},
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/encoding-sniffer": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz",
@@ -1132,6 +1774,36 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -1141,6 +1813,12 @@
"node": ">=6"
}
},
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
"node_modules/escodegen": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
@@ -1193,6 +1871,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
@@ -1220,6 +1907,76 @@
"bare-events": "^2.7.0"
}
},
+ "node_modules/expand-template": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
+ "license": "(MIT OR WTFPL)",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.22.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.3",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.14.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/express/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
"node_modules/extract-zip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
@@ -1296,6 +2053,80 @@
"pend": "~1.2.0"
}
},
+ "node_modules/file-uri-to-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+ "license": "MIT"
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/finalhandler/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/finalhandler/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/fingerprint-generator": {
+ "version": "2.1.82",
+ "resolved": "https://registry.npmjs.org/fingerprint-generator/-/fingerprint-generator-2.1.82.tgz",
+ "integrity": "sha512-5Z/yCKW324pMyMarpIKe/QPdkrFWKNJv3ktdU+fXHri80+HAwNE6QhMvEvsMkK9Q8DeCXZlpPHV77UBa1nFb4A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "generative-bayesian-network": "^2.1.82",
+ "header-generator": "^2.1.82",
+ "tslib": "^2.4.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/for-in": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
+ "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/for-own": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz",
+ "integrity": "sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==",
+ "license": "MIT",
+ "dependencies": {
+ "for-in": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
@@ -1312,6 +2143,73 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+ "license": "MIT"
+ },
+ "node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/geckodriver": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-6.1.0.tgz",
@@ -1333,6 +2231,16 @@
"node": ">=20.0.0"
}
},
+ "node_modules/generative-bayesian-network": {
+ "version": "2.1.82",
+ "resolved": "https://registry.npmjs.org/generative-bayesian-network/-/generative-bayesian-network-2.1.82.tgz",
+ "integrity": "sha512-DH4NrmQheoMaJErdVv2IzaqkbOYSDQZmiZTV6UPDJYRDK2EyPpIQ88XRcYdPeFrUjS1N0Jj25H3HUywoJ1dbow==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "adm-zip": "^0.5.9",
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@@ -1342,6 +2250,30 @@
"node": "6.* || 8.* || >= 10.*"
}
},
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/get-port": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz",
@@ -1354,6 +2286,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
@@ -1383,6 +2328,12 @@
"node": ">= 14"
}
},
+ "node_modules/github-from-package": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
+ "license": "MIT"
+ },
"node_modules/glob": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
@@ -1404,6 +2355,18 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -1425,14 +2388,53 @@
"node": ">=8"
}
},
- "node_modules/htmlfy": {
- "version": "0.8.1",
- "resolved": "https://registry.npmjs.org/htmlfy/-/htmlfy-0.8.1.tgz",
- "integrity": "sha512-xWROBw9+MEGwxpotll0h672KCaLrKKiCYzsyN8ZgL9cQbVumFnyvsk2JqiB9ELAV1GLj1GG/jxZUjV9OZZi/yQ==",
- "license": "MIT"
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
},
- "node_modules/htmlparser2": {
- "version": "10.1.0",
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/header-generator": {
+ "version": "2.1.82",
+ "resolved": "https://registry.npmjs.org/header-generator/-/header-generator-2.1.82.tgz",
+ "integrity": "sha512-4NjPB0+bAKjPoponSmTOkK58IEF2W22sOJA5O48k/MxbCZgOm+jrU4WVR53Z2I6xFgIPkVrQmKtt1LAbWtfqXw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "browserslist": "^4.21.1",
+ "generative-bayesian-network": "^2.1.82",
+ "ow": "^0.28.1",
+ "tslib": "^2.4.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/htmlfy": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/htmlfy/-/htmlfy-0.8.1.tgz",
+ "integrity": "sha512-xWROBw9+MEGwxpotll0h672KCaLrKKiCYzsyN8ZgL9cQbVumFnyvsk2JqiB9ELAV1GLj1GG/jxZUjV9OZZi/yQ==",
+ "license": "MIT"
+ },
+ "node_modules/htmlparser2": {
+ "version": "10.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
"integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
"funding": [
@@ -1462,6 +2464,26 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
@@ -1526,6 +2548,165 @@
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
+ "node_modules/impit": {
+ "version": "0.7.6",
+ "resolved": "https://registry.npmjs.org/impit/-/impit-0.7.6.tgz",
+ "integrity": "sha512-AkS6Gv63+E6GMvBrcRhMmOREKpq5oJ0J5m3xwfkHiEs97UIsbpEqFmW3sFw/sdyOTDGRF5q4EjaLxtb922Ta8g==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "impit-darwin-arm64": "0.7.6",
+ "impit-darwin-x64": "0.7.6",
+ "impit-linux-arm64-gnu": "0.7.6",
+ "impit-linux-arm64-musl": "0.7.6",
+ "impit-linux-x64-gnu": "0.7.6",
+ "impit-linux-x64-musl": "0.7.6",
+ "impit-win32-arm64-msvc": "0.7.6",
+ "impit-win32-x64-msvc": "0.7.6"
+ }
+ },
+ "node_modules/impit-darwin-arm64": {
+ "version": "0.7.6",
+ "resolved": "https://registry.npmjs.org/impit-darwin-arm64/-/impit-darwin-arm64-0.7.6.tgz",
+ "integrity": "sha512-M7NQXkttyzqilWfzVkNCp7hApT69m0etyJkVpHze4bR5z1kJnHhdsb8BSdDv2dzvZL4u1JyqZNxq+qoMn84eUw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/impit-darwin-x64": {
+ "version": "0.7.6",
+ "resolved": "https://registry.npmjs.org/impit-darwin-x64/-/impit-darwin-x64-0.7.6.tgz",
+ "integrity": "sha512-kikTesWirAwJp9JPxzGLoGVc+heBlEabWS5AhTkQedACU153vmuL90OBQikVr3ul2N0LPImvnuB+51wV0zDE6g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/impit-linux-arm64-gnu": {
+ "version": "0.7.6",
+ "resolved": "https://registry.npmjs.org/impit-linux-arm64-gnu/-/impit-linux-arm64-gnu-0.7.6.tgz",
+ "integrity": "sha512-H6GHjVr/0lG9VEJr6IHF8YLq+YkSIOF4k7Dfue2ygzUAj1+jZ5ZwnouhG/XrZHYW6EWsZmEAjjRfWE56Q0wDRQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/impit-linux-arm64-musl": {
+ "version": "0.7.6",
+ "resolved": "https://registry.npmjs.org/impit-linux-arm64-musl/-/impit-linux-arm64-musl-0.7.6.tgz",
+ "integrity": "sha512-1sCB/UBVXLZTpGJsXRdNNSvhN9xmmQcYLMWAAB4Itb7w684RHX1pLoCb6ichv7bfAf6tgaupcFIFZNBp3ghmQA==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/impit-linux-x64-gnu": {
+ "version": "0.7.6",
+ "resolved": "https://registry.npmjs.org/impit-linux-x64-gnu/-/impit-linux-x64-gnu-0.7.6.tgz",
+ "integrity": "sha512-yYhlRnZ4fhKt8kuGe0JK2WSHc8TkR6BEH0wn+guevmu8EOn9Xu43OuRvkeOyVAkRqvFnlZtMyySUo/GuSLz9Gw==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/impit-linux-x64-musl": {
+ "version": "0.7.6",
+ "resolved": "https://registry.npmjs.org/impit-linux-x64-musl/-/impit-linux-x64-musl-0.7.6.tgz",
+ "integrity": "sha512-sdGWyu+PCLmaOXy7Mzo4WP61ZLl5qpZ1L+VeXW+Ycazgu0e7ox0NZLdiLRunIrEzD+h0S+e4CyzNwaiP3yIolg==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/impit-win32-arm64-msvc": {
+ "version": "0.7.6",
+ "resolved": "https://registry.npmjs.org/impit-win32-arm64-msvc/-/impit-win32-arm64-msvc-0.7.6.tgz",
+ "integrity": "sha512-sM5deBqo0EuXg5GACBUMKEua9jIau/i34bwNlfrf/Amnw1n0GB4/RkuUh+sKiUcbNAntrRq+YhCq8qDP8IW19w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/impit-win32-x64-msvc": {
+ "version": "0.7.6",
+ "resolved": "https://registry.npmjs.org/impit-win32-x64-msvc/-/impit-win32-x64-msvc-0.7.6.tgz",
+ "integrity": "sha512-ry63ADGLCB/PU/vNB1VioRt2V+klDJ34frJUXUZBEv1kA96HEAg9AxUk+604o+UHS3ttGH2rkLmrbwHOdAct5Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
"node_modules/import-meta-resolve": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz",
@@ -1536,12 +2717,29 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "license": "ISC"
+ },
"node_modules/ip-address": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
@@ -1551,6 +2749,30 @@
"node": ">= 12"
}
},
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-buffer": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
+ "license": "MIT"
+ },
+ "node_modules/is-extendable": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+ "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@@ -1560,6 +2782,15 @@
"node": ">=8"
}
},
+ "node_modules/is-obj": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
+ "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-plain-obj": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
@@ -1572,6 +2803,38 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "license": "MIT",
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-standalone-pwa": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/is-standalone-pwa/-/is-standalone-pwa-0.1.1.tgz",
+ "integrity": "sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/faisalman"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ua-parser-js"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/faisalman"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
@@ -1599,6 +2862,15 @@
"node": ">=18"
}
},
+ "node_modules/isobject": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+ "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
@@ -1623,6 +2895,18 @@
"jiti": "lib/jiti-cli.mjs"
}
},
+ "node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
@@ -1665,6 +2949,45 @@
"safe-buffer": "~5.1.0"
}
},
+ "node_modules/kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/language-subtag-registry": {
+ "version": "0.3.23",
+ "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
+ "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==",
+ "license": "CC0-1.0"
+ },
+ "node_modules/language-tags": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-2.1.0.tgz",
+ "integrity": "sha512-D4CgpyCt+61f6z2jHjJS1OmZPviAWM57iJ9OKdFFWSNgS7Udj9QVWqyGs/cveVNF57XpZmhSvMdVIV5mjLA7Vg==",
+ "license": "MIT",
+ "dependencies": {
+ "language-subtag-registry": "^0.3.20"
+ },
+ "engines": {
+ "node": ">=22"
+ }
+ },
+ "node_modules/lazy-cache": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz",
+ "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/lazystream": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
@@ -1749,6 +3072,13 @@
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
"license": "MIT"
},
+ "node_modules/lodash.isequal": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+ "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
+ "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
+ "license": "MIT"
+ },
"node_modules/lodash.zip": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz",
@@ -1780,6 +3110,115 @@
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"license": "ISC"
},
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/maxmind": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/maxmind/-/maxmind-5.0.6.tgz",
+ "integrity": "sha512-5bvd/u+kIaTqaGM+xkXjatzQw1dQfSmlLggr2W1EKMyMxSgx2woZyusLpNpZ4DdPmL+1bbJWeo4LXsi6bC0Iew==",
+ "license": "MIT",
+ "dependencies": {
+ "mmdb-lib": "3.0.2",
+ "tiny-lru": "13.0.0"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-deep": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.3.tgz",
+ "integrity": "sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==",
+ "license": "MIT",
+ "dependencies": {
+ "arr-union": "^3.1.0",
+ "clone-deep": "^0.2.4",
+ "kind-of": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/minimatch": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
@@ -1795,6 +3234,15 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/minipass": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
@@ -1810,6 +3258,44 @@
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
+ "node_modules/mixin-object": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz",
+ "integrity": "sha512-ALGF1Jt9ouehcaXaHhn6t1yGWRqGaHkPFndtFVHfZXOvkIZ/yoGaSi0AHVTafb3ZBGg4dr/bDwnaEKqCXzchMA==",
+ "license": "MIT",
+ "dependencies": {
+ "for-in": "^0.1.3",
+ "is-extendable": "^0.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/mixin-object/node_modules/for-in": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz",
+ "integrity": "sha512-F0to7vbBSHP8E3l6dCjxNOLuSFAACIxFy3UehTUlG7svlXi37HHsDkyVcHo0Pq8QwrE+pXvWSVX3ZT1T9wAZ9g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+ "license": "MIT"
+ },
+ "node_modules/mmdb-lib": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mmdb-lib/-/mmdb-lib-3.0.2.tgz",
+ "integrity": "sha512-7e87vk0DdWT647wjcfEtWeMtjm+zVGqNohN/aeIymbUfjHQ2T4Sx5kM+1irVDBSloNC3CkGKxswdMoo8yhqTDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10",
+ "npm": ">=6"
+ }
+ },
"node_modules/modern-tar": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.4.tgz",
@@ -1825,6 +3311,21 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
+ "node_modules/napi-build-utils": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
+ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/netmask": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz",
@@ -1834,6 +3335,24 @@
"node": ">= 0.4.0"
}
},
+ "node_modules/node-abi": {
+ "version": "3.89.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz",
+ "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==",
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.37",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
+ "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
+ "license": "MIT"
+ },
"node_modules/node-simctl": {
"version": "7.7.5",
"resolved": "https://registry.npmjs.org/node-simctl/-/node-simctl-7.7.5.tgz",
@@ -1877,6 +3396,30 @@
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -1886,6 +3429,25 @@
"wrappy": "1"
}
},
+ "node_modules/ow": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/ow/-/ow-0.28.2.tgz",
+ "integrity": "sha512-dD4UpyBh/9m4X2NVjA+73/ZPBRF+uF4zIMFvvQsabMiEK8x41L3rQ8EENOi35kyyoaJwNxEeJcP6Fj1H4U409Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@sindresorhus/is": "^4.2.0",
+ "callsites": "^3.1.0",
+ "dot-prop": "^6.0.1",
+ "lodash.isequal": "^4.5.0",
+ "vali-date": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/pac-proxy-agent": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
@@ -1979,6 +3541,15 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/path-expression-matcher": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz",
@@ -1994,6 +3565,15 @@
"node": ">=14.0.0"
}
},
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -2019,16 +3599,46 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/path-to-regexp": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
+ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
+ "license": "MIT"
+ },
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
"license": "MIT"
},
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/playwright": {
+ "version": "1.59.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
+ "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.59.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
"node_modules/playwright-core": {
- "version": "1.58.0",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.0.tgz",
- "integrity": "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==",
+ "version": "1.59.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
+ "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
@@ -2037,6 +3647,99 @@
"node": ">=18"
}
},
+ "node_modules/playwright-extra": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/playwright-extra/-/playwright-extra-4.3.6.tgz",
+ "integrity": "sha512-q2rVtcE8V8K3vPVF1zny4pvwZveHLH8KBuVU2MoE3Jw4OKVoBWsHI9CH9zPydovHHOCDxjGN2Vg+2m644q3ijA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "playwright": "*",
+ "playwright-core": "*"
+ },
+ "peerDependenciesMeta": {
+ "playwright": {
+ "optional": true
+ },
+ "playwright-core": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/prebuild-install": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
+ "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
+ "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
+ "license": "MIT",
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "expand-template": "^2.0.3",
+ "github-from-package": "0.0.0",
+ "minimist": "^1.2.3",
+ "mkdirp-classic": "^0.5.3",
+ "napi-build-utils": "^2.0.0",
+ "node-abi": "^3.3.0",
+ "pump": "^3.0.0",
+ "rc": "^1.2.7",
+ "simple-get": "^4.0.0",
+ "tar-fs": "^2.0.0",
+ "tunnel-agent": "^0.6.0"
+ },
+ "bin": {
+ "prebuild-install": "bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/prebuild-install/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/prebuild-install/node_modules/tar-fs": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
+ "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.1.4"
+ }
+ },
+ "node_modules/prebuild-install/node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
@@ -2061,6 +3764,19 @@
"node": ">=0.4.0"
}
},
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/proxy-agent": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz",
@@ -2105,12 +3821,243 @@
"once": "^1.3.1"
}
},
- "node_modules/query-selector-shadow-dom": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz",
- "integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==",
- "license": "MIT"
- },
+ "node_modules/puppeteer-extra-plugin": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/puppeteer-extra-plugin/-/puppeteer-extra-plugin-3.2.3.tgz",
+ "integrity": "sha512-6RNy0e6pH8vaS3akPIKGg28xcryKscczt4wIl0ePciZENGE2yoaQJNd17UiEbdmh5/6WW6dPcfRWT9lxBwCi2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/debug": "^4.1.0",
+ "debug": "^4.1.1",
+ "merge-deep": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=9.11.2"
+ },
+ "peerDependencies": {
+ "playwright-extra": "*",
+ "puppeteer-extra": "*"
+ },
+ "peerDependenciesMeta": {
+ "playwright-extra": {
+ "optional": true
+ },
+ "puppeteer-extra": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/puppeteer-extra-plugin-stealth": {
+ "version": "2.11.2",
+ "resolved": "https://registry.npmjs.org/puppeteer-extra-plugin-stealth/-/puppeteer-extra-plugin-stealth-2.11.2.tgz",
+ "integrity": "sha512-bUemM5XmTj9i2ZerBzsk2AN5is0wHMNE6K0hXBzBXOzP5m5G3Wl0RHhiqKeHToe/uIH8AoZiGhc1tCkLZQPKTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "puppeteer-extra-plugin": "^3.2.3",
+ "puppeteer-extra-plugin-user-preferences": "^2.4.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "peerDependencies": {
+ "playwright-extra": "*",
+ "puppeteer-extra": "*"
+ },
+ "peerDependenciesMeta": {
+ "playwright-extra": {
+ "optional": true
+ },
+ "puppeteer-extra": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/puppeteer-extra-plugin-user-data-dir": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/puppeteer-extra-plugin-user-data-dir/-/puppeteer-extra-plugin-user-data-dir-2.4.1.tgz",
+ "integrity": "sha512-kH1GnCcqEDoBXO7epAse4TBPJh9tEpVEK/vkedKfjOVOhZAvLkHGc9swMs5ChrJbRnf8Hdpug6TJlEuimXNQ+g==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "fs-extra": "^10.0.0",
+ "puppeteer-extra-plugin": "^3.2.3",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "peerDependencies": {
+ "playwright-extra": "*",
+ "puppeteer-extra": "*"
+ },
+ "peerDependenciesMeta": {
+ "playwright-extra": {
+ "optional": true
+ },
+ "puppeteer-extra": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/puppeteer-extra-plugin-user-data-dir/node_modules/brace-expansion": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/puppeteer-extra-plugin-user-data-dir/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/puppeteer-extra-plugin-user-data-dir/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/puppeteer-extra-plugin-user-data-dir/node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
+ "license": "ISC",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/puppeteer-extra-plugin-user-preferences": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/puppeteer-extra-plugin-user-preferences/-/puppeteer-extra-plugin-user-preferences-2.4.1.tgz",
+ "integrity": "sha512-i1oAZxRbc1bk8MZufKCruCEC3CCafO9RKMkkodZltI4OqibLFXF3tj6HZ4LZ9C5vCXZjYcDWazgtY69mnmrQ9A==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "deepmerge": "^4.2.2",
+ "puppeteer-extra-plugin": "^3.2.3",
+ "puppeteer-extra-plugin-user-data-dir": "^2.4.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "peerDependencies": {
+ "playwright-extra": "*",
+ "puppeteer-extra": "*"
+ },
+ "peerDependenciesMeta": {
+ "playwright-extra": {
+ "optional": true
+ },
+ "puppeteer-extra": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.14.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
+ "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/query-selector-shadow-dom": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz",
+ "integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==",
+ "license": "MIT"
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/raw-body/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
"node_modules/readable-stream": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
@@ -2250,6 +4197,15 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
+ "node_modules/sax": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
+ "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=11.0.0"
+ }
+ },
"node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
@@ -2262,6 +4218,45 @@
"node": ">=10"
}
},
+ "node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/send/node_modules/debug/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
"node_modules/serialize-error": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-12.0.0.tgz",
@@ -2289,6 +4284,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
@@ -2301,6 +4311,48 @@
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/shallow-clone": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz",
+ "integrity": "sha512-J1zdXCky5GmNnuauESROVu31MQSnLoYvlyEn6j2Ztk6Q5EHFIhxkMhYcv6vuDzl2XEzoRr856QwzMgWM/TmZgw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-extendable": "^0.1.1",
+ "kind-of": "^2.0.1",
+ "lazy-cache": "^0.2.3",
+ "mixin-object": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/shallow-clone/node_modules/kind-of": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz",
+ "integrity": "sha512-0u8i1NZ/mg0b+W3MGGw5I7+6Eib2nx72S/QvXa0hYjEkjTknYmEYQJwGu3mLC0BrhtJjtQafTkyRUQ75Kx0LVg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-buffer": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/shallow-clone/node_modules/lazy-cache": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz",
+ "integrity": "sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -2334,6 +4386,78 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
@@ -2346,6 +4470,51 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/simple-concat": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/simple-get": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+ "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decompress-response": "^6.0.0",
+ "once": "^1.3.1",
+ "simple-concat": "^1.0.0"
+ }
+ },
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
@@ -2428,6 +4597,15 @@
"node": ">= 10.x"
}
},
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/streamx": {
"version": "2.23.0",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
@@ -2544,6 +4722,15 @@
"node": ">=8"
}
},
+ "node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/strnum": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz",
@@ -2628,12 +4815,42 @@
"b4a": "^1.6.4"
}
},
+ "node_modules/tiny-lru": {
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-13.0.0.tgz",
+ "integrity": "sha512-xDHxKKS1FdF0Tv2P+QT7IeSEg74K/8cEDzbv3Tv6UyHHUgBOjOiQiBp818MGj66dhurQus/IBcoAbwIKtSGc6Q==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
+ "node_modules/tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/type-fest": {
"version": "4.26.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.0.tgz",
@@ -2646,6 +4863,70 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ua-is-frozen": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/ua-is-frozen/-/ua-is-frozen-0.1.2.tgz",
+ "integrity": "sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/faisalman"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ua-parser-js"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/faisalman"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/ua-parser-js": {
+ "version": "2.0.9",
+ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-2.0.9.tgz",
+ "integrity": "sha512-OsqGhxyo/wGdLSXMSJxuMGN6H4gDnKz6Fb3IBm4bxZFMnyy0sdf6MN96Ie8tC6z/btdO+Bsy8guxlvLdwT076w==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ua-parser-js"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/faisalman"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/faisalman"
+ }
+ ],
+ "license": "AGPL-3.0-or-later",
+ "dependencies": {
+ "detect-europe-js": "^0.1.2",
+ "is-standalone-pwa": "^0.1.1",
+ "ua-is-frozen": "^0.1.2"
+ },
+ "bin": {
+ "ua-parser-js": "script/cli.js"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/undici": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.6.tgz",
@@ -2661,6 +4942,54 @@
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT"
},
+ "node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
"node_modules/urlpattern-polyfill": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz",
@@ -2682,6 +5011,15 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
@@ -2695,6 +5033,24 @@
"uuid": "dist/esm/bin/uuid"
}
},
+ "node_modules/vali-date": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz",
+ "integrity": "sha512-sgECfZthyaCKW10N0fm27cg8HYTFK5qMWgypqkXMQ4Wbl/zZKx7xZICgcoxIIE+WFAP/MBL2EFwC/YvLxw3Zeg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/wait-port": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/wait-port/-/wait-port-1.1.0.tgz",
@@ -2973,6 +5329,28 @@
}
}
},
+ "node_modules/xml2js": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
+ "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
+ "license": "MIT",
+ "dependencies": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/xmlbuilder": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
diff --git a/plans/podman-backend-configuration.md b/plans/podman-backend-configuration.md
new file mode 100644
index 0000000000000..747af378d15ab
--- /dev/null
+++ b/plans/podman-backend-configuration.md
@@ -0,0 +1,302 @@
+# Podman Backend Configuration Implementation Plan
+
+## Overview
+
+Add support for user namespace remapping, rootless mode, and other Podman-specific options as configuration options in Hermes Agent. This includes integrating Podman as a selectable terminal backend.
+
+## Configuration Schema
+
+Add these options to `terminal:` section in `config.yaml`:
+
+```yaml
+terminal:
+ # Podman-specific options
+ podman_userns: "" # --userns flag (e.g., "keep-id", "auto")
+ podman_user: "" # --user flag (e.g., "1000:1000", "nonroot")
+ podman_privileged: false # --privileged flag
+ podman_extra_capabilities: [] # Additional --cap-add values (additive to defaults)
+ podman_extra_args: [] # Arbitrary additional podman run flags
+ podman_rootful: false # Run podman commands with sudo (i.e. rootful mode)
+```
+
+### Environment Variables
+
+Each config option can also be set via environment variable:
+
+| Config Key | Environment Variable | Type | Default |
+|-------------|---------------------|--------|----------|
+| `podman_userns` | `TERMINAL_PODMAN_USERNS` | string | `""` |
+| `podman_user` | `TERMINAL_PODMAN_USER` | string | `""` |
+| `podman_privileged` | `TERMINAL_PODMAN_PRIVILEGED` | boolean | `false` |
+| `podman_extra_capabilities` | `TERMINAL_PODMAN_EXTRA_CAPABILITIES` | JSON array | `[]` |
+| `podman_extra_args` | `TERMINAL_PODMAN_EXTRA_ARGS` | JSON array | `[]` |
+| `podman_rootful` | `TERMINAL_PODMAN_ROOTFUL` | boolean | `false` |
+
+## Implementation Details
+
+### 1. Configuration Options in `hermes_cli/config.py`
+
+#### Add to `DEFAULT_CONFIG["terminal"]`:
+
+```python
+"podman_userns": "", # --userns flag
+"podman_user": "", # --user flag
+"podman_privileged": False, # --privileged flag
+"podman_extra_capabilities": [], # Additional --cap-add values
+"podman_extra_args": [], # Arbitrary podman run flags
+"podman_rootful": False, # Run podman with sudo
+```
+
+#### Add to `OPTIONAL_ENV_VARS`:
+
+```python
+"TERMINAL_PODMAN_USERNS": {
+ "description": "Podman user namespace mode (--userns flag)",
+ "prompt": "Podman User Namespace",
+ "url": "https://docs.podman.io/en/latest/markdown/podman-run.1.html#userns-mode",
+ "password": False,
+ "category": "tool",
+},
+"TERMINAL_PODMAN_USER": {
+ "description": "Podman user to run as inside container (--user flag)",
+ "prompt": "Podman User",
+ "url": "https://docs.podman.io/en/latest/markdown/podman-run.1.html#user-user",
+ "password": False,
+ "category": "tool",
+},
+"TERMINAL_PODMAN_PRIVILEGED": {
+ "description": "Run Podman containers in privileged mode (--privileged flag)",
+ "prompt": "Podman Privileged Mode",
+ "url": "https://docs.podman.io/en/latest/markdown/podman-run.1.html#privileged",
+ "password": False,
+ "category": "tool",
+},
+"TERMINAL_PODMAN_EXTRA_CAPABILITIES": {
+ "description": "Additional Linux capabilities to add to Podman containers (--cap-add)",
+ "prompt": "Podman Extra Capabilities",
+ "url": "https://docs.podman.io/en/latest/markdown/podman-run.1.html#cap-add-capability",
+ "password": False,
+ "category": "tool",
+},
+"TERMINAL_PODMAN_EXTRA_ARGS": {
+ "description": "Additional arbitrary arguments for podman run (JSON array)",
+ "prompt": "Podman Extra Arguments",
+ "url": "https://docs.podman.io/en/latest/markdown/podman-run.1.html",
+ "password": False,
+ "category": "tool",
+},
+"TERMINAL_PODMAN_ROOTFUL": {
+ "description": "Run Podman commands with sudo",
+ "prompt": "Podman Use Sudo",
+ "url": "",
+ "password": False,
+ "category": "tool",
+},
+```
+
+### 2. Update `PodmanEnvironment` in `tools/environments/podman.py`
+
+#### Modify `__init__` signature (line 155):
+
+```python
+def __init__(
+ self,
+ image: str,
+ cwd: str = "/root",
+ timeout: int = 60,
+ cpu: float = 0,
+ memory: int = 0,
+ disk: int = 0,
+ persistent_filesystem: bool = False,
+ task_id: str = "default",
+ volumes: list = None,
+ forward_env: list[str] | None = None,
+ env: dict | None = None,
+ network: bool = True,
+ host_cwd: str = None,
+ auto_mount_cwd: bool = False,
+ # New Podman-specific options
+ userns: str = "",
+ user: str = "",
+ privileged: bool = False,
+ extra_capabilities: list = None,
+ extra_args: list = None,
+ rootful: bool = False,
+):
+```
+
+#### Store new instance variables (after line 180):
+
+```python
+self._privileged = privileged
+self._userns = userns
+self._user = user
+self._extra_capabilities = extra_capabilities or []
+self._extra_args = extra_args or []
+self._rootful = rootful
+```
+
+#### Apply options when building `run_cmd` (before line 334):
+
+```python
+# Apply privileged flag
+if self._privileged:
+ all_run_args.append("--privileged")
+
+# Apply user namespace
+if self._userns:
+ all_run_args.extend(["--userns", self._userns])
+
+# Apply user
+if self._user:
+ all_run_args.extend(["--user", self._user])
+
+# Apply extra capabilities (additive to defaults)
+if self._extra_capabilities:
+ for cap in self._extra_capabilities:
+ all_run_args.extend(["--cap-add", cap])
+
+# Apply extra args (no validation)
+if self._extra_args:
+ all_run_args.extend(self._extra_args)
+```
+
+#### Build command with sudo support (line 334):
+
+```python
+# Build the podman run command with sudo if needed
+podman_exe = find_podman() or "podman"
+if self._rootful:
+ run_cmd = [
+ "sudo", podman_exe, "run", "-d",
+ "--name", container_name,
+ "-w", cwd,
+ *all_run_args,
+ image,
+ "sleep", "2h",
+ ]
+else:
+ run_cmd = [
+ podman_exe, "run", "-d",
+ "--name", container_name,
+ "-w", cwd,
+ *all_run_args,
+ image,
+ "sleep", "2h",
+ ]
+```
+
+#### Update `execute()` method for sudo support (line 378):
+
+```python
+assert self._container_id, "Container not started"
+cmd = [self._podman_exe, "exec"]
+if self._rootful:
+ cmd = ["sudo"] + cmd
+if effective_stdin is not None:
+ cmd.append("-i")
+cmd.extend(["-w", work_dir])
+```
+
+#### Update `cleanup()` for sudo support (line 459):
+
+```python
+if self._container_id:
+ try:
+ # Stop in background so cleanup doesn't block
+ sudo_prefix = "sudo " if self._rootful else ""
+ stop_cmd = (
+ f"(timeout 60 {sudo_prefix}{self._podman_exe} stop {self._container_id} || "
+ f"{sudo_prefix}{self._podman_exe} rm -f {self._container_id}) >/dev/null 2>&1 &"
+ )
+ subprocess.Popen(stop_cmd, shell=True)
+```
+
+### 3. Terminal Tool Integration in `tools/terminal_tool.py`
+
+#### Import PodmanEnvironment (line ~408):
+
+```python
+from tools.environments.podman import PodmanEnvironment as _PodmanEnvironment
+```
+
+#### Add Podman config to `_get_env_config()` (after line 575):
+
+```python
+# Podman-specific config
+"podman_userns": os.getenv("TERMINAL_PODMAN_USERNS", ""),
+"podman_user": os.getenv("TERMINAL_PODMAN_USER", ""),
+"podman_privileged": os.getenv("TERMINAL_PODMAN_PRIVILEGED", "false").lower() in ("true", "1", "yes"),
+"podman_extra_capabilities": _parse_env_var("TERMINAL_PODMAN_EXTRA_CAPABILITIES", "[]", json.loads, "valid JSON"),
+"podman_extra_args": _parse_env_var("TERMINAL_PODMAN_EXTRA_ARGS", "[]", json.loads, "valid JSON"),
+"podman_rootful": os.getenv("TERMINAL_PODMAN_ROOTFUL", "false").lower() in ("true", "1", "yes"),
+```
+
+#### Add Podman case to `_create_environment()` (after line 632):
+
+```python
+elif env_type == "podman":
+ return _PodmanEnvironment(
+ image=image, cwd=cwd, timeout=timeout,
+ cpu=cpu, memory=memory, disk=disk,
+ persistent_filesystem=persistent, task_id=task_id,
+ volumes=volumes,
+ host_cwd=host_cwd,
+ auto_mount_cwd=cc.get("docker_mount_cwd_to_workspace", False),
+ forward_env=docker_forward_env,
+ env=docker_env,
+ network=cc.get("network", True),
+ # Podman-specific options
+ userns=cc.get("podman_userns", ""),
+ user=cc.get("podman_user", ""),
+ privileged=cc.get("podman_privileged", False),
+ extra_capabilities=cc.get("podman_extra_capabilities", []),
+ extra_args=cc.get("podman_extra_args", []),
+ rootful=cc.get("podman_rootful", False),
+ )
+```
+
+#### Update error message (line 716):
+
+```python
+raise ValueError(f"Unknown environment type: {env_type}. Use 'local', 'docker', 'podman', 'singularity', 'modal', 'daytona', or 'ssh'")
+```
+
+#### Update cwd handling for Podman (line 533):
+
+```python
+elif env_type in ("modal", "docker", "podman", "singularity", "daytona") and cwd:
+```
+
+## Files to Modify
+
+| File | Path | Changes |
+|-------|-------|----------|
+| Config | `hermes_cli/config.py` | Add 6 config options to `DEFAULT_CONFIG["terminal"]` and 6 entries to `OPTIONAL_ENV_VARS` |
+| Podman Environment | `tools/environments/podman.py` | Update `__init__` signature, add instance variables, apply options to run/exec/cleanup commands |
+| Terminal Tool | `tools/terminal_tool.py` | Import PodmanEnvironment, add config reading, add podman case to _create_environment |
+
+## Migration
+
+No migration needed. These are new configuration options with safe defaults (empty strings, False, empty lists). Existing users will not be affected.
+
+## Testing
+
+After implementation, test:
+
+1. **Basic Podman backend**: `TERMINAL_ENV=podman hermes`
+2. **User namespace remapping**: Set `podman_userns: "keep-id"` in config
+3. **Custom user**: Set `podman_user: "1000:1000"` in config
+4. **Privileged mode**: Set `podman_privileged: true` in config
+5. **Extra capabilities**: Set `podman_extra_capabilities: ["NET_ADMIN"]` in config
+6. **Extra args**: Set `podman_extra_args: ["--rm", "--shm-size", "512m"]` in config
+7. **Rootful support**: Set `podman_rootful: true` in config
+8. **Environment variables**: Test each option via environment variable instead of config
+
+## Notes
+
+- `podman_extra_args` provides maximum flexibility for power users
+- No validation or warnings for `podman_extra_args` - use at your own risk
+- `podman_extra_capabilities` is additive to existing `DAC_OVERRIDE`, `CHOWN`, `FOWNER`
+- Sudo is assumed to be in `$PATH` - if not found, exception will be caught and reported
+- Rootless mode is implicit (determined by who runs Hermes), not a configuration option
diff --git a/plans/podman-baseenvironment-migration.md b/plans/podman-baseenvironment-migration.md
new file mode 100644
index 0000000000000..b2896ec91df9f
--- /dev/null
+++ b/plans/podman-baseenvironment-migration.md
@@ -0,0 +1,296 @@
+# PodmanEnvironment BaseEnvironment Migration Plan
+
+## Overview
+
+Migrate `PodmanEnvironment` to use the unified `BaseEnvironment.execute()` pattern, matching the implementation in `DockerEnvironment`. This ensures consistent session management, CWD tracking, and command execution flow across all container backends.
+
+## Background
+
+After a rebase against upstream, `BaseEnvironment` now has a unified `execute()` method that provides:
+
+- Session snapshot sourcing (env vars, functions, aliases persist across commands)
+- CWD tracking via stdout markers
+- Login shell fallback when snapshot fails
+- Unified interrupt/timeout handling
+- Stdin heredoc embedding for SDK backends
+
+## Current State Analysis
+
+### BaseEnvironment.execute() (tools/environments/base.py:489-528)
+
+Provides unified execution flow:
+1. Calls `_before_execute()` hook
+2. Prepares command with `_prepare_command()` (sudo transformation)
+3. Merges sudo stdin with caller stdin
+4. Embeds stdin as heredoc for backends that need it
+5. Wraps command with `_wrap_command()` (sources snapshot, cd's, runs command, re-dumps env, emits CWD markers)
+6. Uses login shell if snapshot failed
+7. Calls abstract `_run_bash()` method
+8. Waits for process with `_wait_for_process()` (handles interrupts, timeouts, stdout draining)
+9. Updates CWD with `_update_cwd()`
+
+### PodmanEnvironment.execute() (tools/environments/podman.py:398-501) - TO BE REMOVED
+
+Current implementation has:
+- ✓ Calls `_before_execute()` hook
+- ✓ Prepares command with `_prepare_command()`
+- ✓ Merges sudo stdin with caller stdin
+- ✗ **Special handling for `~` expansion** - prepends `cd` to command and sets cwd to `/`
+- ✗ **Per-exec environment variable injection** - builds env args for every execute call
+- ✗ **Custom process spawning** - directly builds `podman exec` command
+- ✗ **Custom drain thread and wait loop** - duplicates `_wait_for_process()` logic
+- ✗ **No session snapshot sourcing** - doesn't use `_wrap_command()`
+- ✗ **No CWD marker emission/parsing** - doesn't use CWD markers
+
+### DockerEnvironment Pattern (Reference)
+
+`DockerEnvironment` correctly uses base class pattern:
+- Implements `_run_bash()` abstract method (tools/environments/docker.py:396-417)
+- Implements `_build_init_env_args()` for init-time env injection (tools/environments/docker.py:364-394)
+- Calls `self.init_session()` in `__init__` (line 362)
+- **Does NOT override `execute()`** - uses base class implementation
+- Uses `_popen_bash()` helper from base
+
+## Key Differences
+
+| Feature | BaseEnvironment | PodmanEnvironment (current) |
+|---------|-----------------|-----------------------------|
+| Session snapshot sourcing | Yes (via `_wrap_command`) | No |
+| CWD tracking via markers | Yes (via `_wrap_command`) | No |
+| Per-exec env injection | No (only at init via snapshot) | Yes (every execute call) |
+| Process waiting | `_wait_for_process()` | Custom implementation |
+| Stdin handling | Supports pipe/heredoc modes | Pipe only |
+| Login shell fallback | Yes (if snapshot failed) | No |
+| Uses base `execute()` | N/A (base class) | No (overrides) |
+
+## Migration Plan
+
+### Step 1: Remove custom execute() method
+
+**Remove lines 398-501** from `tools/environments/podman.py`
+
+The entire `execute()` method will be deleted and replaced by base class implementation.
+
+### Step 2: Implement _run_bash() method
+
+**Add after line 397** (after `__init__` method, before `cleanup()`):
+
+```python
+def _run_bash(self, cmd_string: str, *, login: bool = False,
+ timeout: int = 120,
+ stdin_data: str | None = None) -> subprocess.Popen:
+ """Spawn a bash process inside Podman container."""
+ assert self._container_id, "Container not started"
+ cmd = [self._podman_exe, "exec"]
+
+ # Rootful support: prefix with sudo if needed
+ if self._rootful:
+ cmd = ["sudo"] + cmd
+
+ if stdin_data is not None:
+ cmd.append("-i")
+
+ # Only inject -e env args during init_session (login=True).
+ # Subsequent commands get env vars from the snapshot file.
+ if login:
+ cmd.extend(self._init_env_args)
+
+ cmd.extend([self._container_id])
+
+ if login:
+ cmd.extend(["bash", "-l", "-c", cmd_string])
+ else:
+ cmd.extend(["bash", "-c", cmd_string])
+
+ return _popen_bash(cmd, stdin_data)
+```
+
+**Key points:**
+- Uses `_popen_bash()` helper from base class (need to import it)
+- Handles rootful mode by prefixing `sudo` to the command
+- Only injects env args during `login=True` (init_session)
+- Uses `-i` flag when stdin_data is provided
+- Uses `bash -l` for login shell (init_session), `bash -c` for regular commands
+
+### Step 3: Implement _build_init_env_args() method
+
+**Add after `_run_bash()` method**:
+
+```python
+def _build_init_env_args(self) -> list[str]:
+ """Build -e KEY=VALUE args for injecting host env vars into init_session.
+
+ These are used once during init_session() so that export -p captures
+ them into the snapshot. Subsequent execute() calls don't need -e flags.
+ """
+ from tools.environments.utils import _load_hermes_env_vars
+ from tools.environments.local import _HERMES_PROVIDER_ENV_BLOCKLIST
+
+ exec_env: dict[str, str] = dict(self._env)
+
+ forward_keys = set(self._forward_env)
+ passthrough_keys: set[str] = set()
+ try:
+ from tools.env_passthrough import get_all_passthrough
+ passthrough_keys = set(get_all_passthrough())
+ except Exception:
+ pass
+
+ # Explicit docker_forward_env entries are an intentional opt-in and must
+ # win over the generic Hermes secret blocklist. Only implicit passthrough
+ # keys are filtered.
+ forward_keys = forward_keys | (passthrough_keys - _HERMES_PROVIDER_ENV_BLOCKLIST)
+ hermes_env = _load_hermes_env_vars() if forward_keys else {}
+ for key in sorted(forward_keys):
+ value = os.getenv(key)
+ if value is None:
+ value = hermes_env.get(key)
+ if value is not None:
+ exec_env[key] = value
+
+ args = []
+ for key in sorted(exec_env):
+ args.extend(["-e", f"{key}={exec_env[key]}"])
+ return args
+```
+
+**Key points:**
+- Mirrors `DockerEnvironment._build_init_env_args()` implementation
+- Combines explicit forward env with passthrough env
+- Filters out Hermes provider env blocklist for implicit passthrough keys
+- Returns list of `-e KEY=VALUE` arguments
+
+### Step 4: Add init_session() call to __init__
+
+**Add after line 396** (after container starts, after `logger.info` line):
+
+```python
+# Build init-time env forwarding args (used only by init_session
+# to inject host env vars into the snapshot; subsequent commands get
+# them from the snapshot file).
+self._init_env_args = self._build_init_env_args()
+
+# Initialize session snapshot inside container
+self.init_session()
+```
+
+### Step 5: Add import for _popen_bash
+
+**Modify line 21** (imports section):
+
+```python
+from tools.environments.base import BaseEnvironment, _popen_bash
+```
+
+### Step 6: Update cleanup() for rootful support
+
+The `cleanup()` method already handles rootful support (lines 508-527), but ensure it's consistent:
+
+```python
+def cleanup(self):
+ """Stop and remove the container. Bind-mount dirs persist if persistent=True."""
+ if self._container_id:
+ try:
+ # Stop in background so cleanup doesn't block
+ sudo_prefix = "sudo " if self._rootful else ""
+ stop_cmd = (
+ f"(timeout 60 {sudo_prefix}{self._podman_exe} stop {self._container_id} || "
+ f"{sudo_prefix}{self._podman_exe} rm -f {self._container_id}) >/dev/null 2>&1 &"
+ )
+ subprocess.Popen(stop_cmd, shell=True)
+ except Exception as e:
+ logger.warning("Failed to stop container %s: %s", self._container_id, e)
+
+ if not self._persistent:
+ # Also schedule removal (stop only leaves it as stopped)
+ sudo_prefix = "sudo " if self._rootful else ""
+ try:
+ subprocess.Popen(
+ f"sleep 3 && {sudo_prefix}{self._podman_exe} rm -f {self._container_id} >/dev/null 2>&1 &",
+ shell=True,
+ )
+ except Exception:
+ pass
+ self._container_id = None
+
+ if not self._persistent:
+ for d in (self._workspace_dir, self._home_dir):
+ if d:
+ shutil.rmtree(d, ignore_errors=True)
+```
+
+This is already correct - no changes needed.
+
+## Special Considerations
+
+### ~ Expansion Handling
+
+The current podman code has special handling for `~` in cwd (lines 417-422):
+
+```python
+if effective_cwd == "~":
+ exec_command = f"cd ~ && {exec_command}"
+ effective_cwd = "/"
+elif effective_cwd.startswith("~/"):
+ exec_command = f"cd ~/{shlex.quote(effective_cwd[2:])} && {exec_command}"
+ effective_cwd = "/"
+```
+
+**This logic should be preserved in `_run_bash()`** OR verified that `_wrap_command()` in base class handles it correctly.
+
+Looking at `BaseEnvironment._wrap_command()` (tools/environments/base.py:317-353), it uses:
+
+```python
+quoted_cwd = (
+ shlex.quote(cwd) if cwd != "~" and not cwd.startswith("~/") else cwd
+)
+parts.append(f"cd {quoted_cwd} || exit 126")
+```
+
+This should handle `~` correctly since it's passed unquoted to bash, which will expand it natively. However, the podman-specific logic may need to stay in `_run_bash()` if there are podman-specific issues with `-w` flag not expanding `~`.
+
+**Recommendation:** Test whether the base class `_wrap_command()` handles `~` correctly. If not, add the special handling to `_run_bash()` before passing to `_popen_bash()`.
+
+### Environment Injection Behavior Change
+
+**Important:** The current podman code injects env vars on EVERY execute call. The base class pattern injects them ONCE during init_session and then relies on the snapshot.
+
+This is the correct behavior - env vars should be captured in the snapshot and re-sourced for subsequent commands. This matches how Docker works and provides better session state persistence.
+
+## Files to Modify
+
+| File | Changes |
+|-------|----------|
+| `tools/environments/podman.py` | 1. Import `_popen_bash` from base
2. Remove `execute()` method (lines 398-501)
3. Add `_run_bash()` method
4. Add `_build_init_env_args()` method
5. Add `self.init_session()` call in `__init__`
6. Add `self._init_env_args` initialization |
+
+## Benefits of Migration
+
+1. **Consistent behavior** across all container backends (Docker, Podman, Singularity)
+2. **Session state persistence** - env vars, functions, aliases survive across commands
+3. **CWD tracking** - `cd` commands properly persist via markers
+4. **Less code duplication** - Reuse base class logic (~100 lines removed)
+5. **Better error handling** - Login shell fallback when snapshot fails
+6. **Maintainability** - Single source of truth for execution logic
+
+## Testing Checklist
+
+After implementation, test:
+
+1. **Basic command execution** - Verify simple commands work
+2. **Session state persistence** - Set env var, verify it persists across commands
+3. **CWD tracking** - `cd` to directory, verify subsequent commands run in that directory
+4. **`~` expansion** - Test commands with `~` and `~/path` as cwd
+5. **Rootful mode** - Test with `podman_rootful: true`
+6. **Snapshot failure fallback** - Simulate snapshot failure, verify login shell fallback works
+7. **Interrupt handling** - Verify Ctrl+C properly interrupts running commands
+8. **Timeout handling** - Verify long-running commands timeout correctly
+9. **Stdin handling** - Test commands that read from stdin
+10. **Environment forwarding** - Verify `docker_forward_env` and env passthrough work correctly
+
+## Summary
+
+The migration will make `PodmanEnvironment` behave identically to `DockerEnvironment` in terms of session management and command execution flow. The rootful podman support will be handled within the `_run_bash()` method, similar to how it's currently handled in `cleanup()` for container removal.
+
+**Total lines removed:** ~100 (the entire `execute()` method)
+**Total lines added:** ~80 (`_run_bash()` + `_build_init_env_args()` + init call)
+**Net reduction:** ~20 lines of code
diff --git a/skills/autonomous-ai-agents/hermes-agent/SKILL.md b/skills/autonomous-ai-agents/hermes-agent/SKILL.md
index 6d8cd1c617038..9db9b4cb71ce4 100644
--- a/skills/autonomous-ai-agents/hermes-agent/SKILL.md
+++ b/skills/autonomous-ai-agents/hermes-agent/SKILL.md
@@ -308,7 +308,7 @@ Edit with `hermes config edit` or `hermes config set section.key value`.
|---------|-------------|
| `model` | `default`, `provider`, `base_url`, `api_key`, `context_length` |
| `agent` | `max_turns` (90), `tool_use_enforcement` |
-| `terminal` | `backend` (local/docker/ssh/modal), `cwd`, `timeout` (180) |
+| `terminal` | `backend` (local/docker/podman/ssh/modal), `cwd`, `timeout` (180) |
| `compression` | `enabled`, `threshold` (0.50), `target_ratio` (0.20) |
| `display` | `skin`, `tool_progress`, `show_reasoning`, `show_cost` |
| `stt` | `enabled`, `provider` (local/groq/openai) |
diff --git a/tests/tools/test_docker_environment.py b/tests/tools/test_docker_environment.py
index e19229a795e88..ddc1e511ff27b 100644
--- a/tests/tools/test_docker_environment.py
+++ b/tests/tools/test_docker_environment.py
@@ -6,7 +6,7 @@
import pytest
-from tools.environments import docker as docker_env
+from tools.environments import docker as docker_env, utils
def _mock_subprocess_run(monkeypatch):
@@ -263,7 +263,7 @@ def test_init_env_args_uses_hermes_dotenv_for_allowlisted_env(monkeypatch):
env = _make_execute_only_env(["DATABASE_URL"])
monkeypatch.delenv("DATABASE_URL", raising=False)
- monkeypatch.setattr(docker_env, "_load_hermes_env_vars", lambda: {"DATABASE_URL": "value_from_dotenv"})
+ monkeypatch.setattr(utils, "load_hermes_env_vars", lambda: {"DATABASE_URL": "value_from_dotenv"})
args = env._build_init_env_args()
args_str = " ".join(args)
@@ -276,7 +276,7 @@ def test_init_env_args_prefers_shell_env_over_hermes_dotenv(monkeypatch):
env = _make_execute_only_env(["DATABASE_URL"])
monkeypatch.setenv("DATABASE_URL", "value_from_shell")
- monkeypatch.setattr(docker_env, "_load_hermes_env_vars", lambda: {"DATABASE_URL": "value_from_dotenv"})
+ monkeypatch.setattr(utils, "load_hermes_env_vars", lambda: {"DATABASE_URL": "value_from_dotenv"})
args = env._build_init_env_args()
args_str = " ".join(args)
@@ -320,7 +320,7 @@ def test_forward_env_overrides_docker_env_in_init_args(monkeypatch):
env._env = {"MY_KEY": "static_value"}
monkeypatch.setenv("MY_KEY", "dynamic_value")
- monkeypatch.setattr(docker_env, "_load_hermes_env_vars", lambda: {})
+ monkeypatch.setattr(utils, "load_hermes_env_vars", lambda: {})
args = env._build_init_env_args()
args_str = " ".join(args)
@@ -335,7 +335,7 @@ def test_docker_env_and_forward_env_merge_in_init_args(monkeypatch):
env._env = {"SSH_AUTH_SOCK": "/run/user/1000/agent.sock"}
monkeypatch.setenv("TOKEN", "secret123")
- monkeypatch.setattr(docker_env, "_load_hermes_env_vars", lambda: {})
+ monkeypatch.setattr(utils, "load_hermes_env_vars", lambda: {})
args = env._build_init_env_args()
args_str = " ".join(args)
@@ -347,7 +347,7 @@ def test_docker_env_and_forward_env_merge_in_init_args(monkeypatch):
def test_normalize_env_dict_filters_invalid_keys():
"""_normalize_env_dict should reject invalid variable names."""
- result = docker_env._normalize_env_dict({
+ result = utils.normalize_env_dict({
"VALID_KEY": "ok",
"123bad": "rejected",
"": "rejected",
@@ -359,7 +359,7 @@ def test_normalize_env_dict_filters_invalid_keys():
def test_normalize_env_dict_coerces_scalars():
"""_normalize_env_dict should coerce int/float/bool to str."""
- result = docker_env._normalize_env_dict({
+ result = utils.normalize_env_dict({
"PORT": 8080,
"DEBUG": True,
"RATIO": 0.5,
@@ -369,14 +369,14 @@ def test_normalize_env_dict_coerces_scalars():
def test_normalize_env_dict_rejects_non_dict():
"""_normalize_env_dict should return empty dict for non-dict input."""
- assert docker_env._normalize_env_dict("not a dict") == {}
- assert docker_env._normalize_env_dict(None) == {}
- assert docker_env._normalize_env_dict([]) == {}
+ assert utils.normalize_env_dict("not a dict") == {}
+ assert utils.normalize_env_dict(None) == {}
+ assert utils.normalize_env_dict([]) == {}
def test_normalize_env_dict_rejects_complex_values():
"""_normalize_env_dict should reject list/dict values."""
- result = docker_env._normalize_env_dict({
+ result = utils.normalize_env_dict({
"GOOD": "string",
"BAD_LIST": [1, 2, 3],
"BAD_DICT": {"nested": True},
diff --git a/tests/tools/test_docker_find.py b/tests/tools/test_docker_find.py
index c1fb58a3edaa1..0a925cc42a4c9 100644
--- a/tests/tools/test_docker_find.py
+++ b/tests/tools/test_docker_find.py
@@ -18,7 +18,7 @@ def _reset_cache():
class TestFindDocker:
def test_found_via_shutil_which(self):
- with patch("tools.environments.docker.shutil.which", return_value="/usr/bin/docker"):
+ with patch("tools.environments.utils.shutil.which", return_value="/usr/bin/docker"):
result = docker_mod.find_docker()
assert result == "/usr/bin/docker"
@@ -28,21 +28,21 @@ def test_not_in_path_falls_back_to_known_locations(self, tmp_path):
fake_docker.write_text("#!/bin/sh\n")
fake_docker.chmod(0o755)
- with patch("tools.environments.docker.shutil.which", return_value=None), \
+ with patch("tools.environments.utils.shutil.which", return_value=None), \
patch("tools.environments.docker._DOCKER_SEARCH_PATHS", [str(fake_docker)]):
result = docker_mod.find_docker()
assert result == str(fake_docker)
def test_returns_none_when_not_found(self):
- with patch("tools.environments.docker.shutil.which", return_value=None), \
+ with patch("tools.environments.utils.shutil.which", return_value=None), \
patch("tools.environments.docker._DOCKER_SEARCH_PATHS", ["/nonexistent/docker"]):
result = docker_mod.find_docker()
assert result is None
def test_caches_result(self):
- with patch("tools.environments.docker.shutil.which", return_value="/usr/local/bin/docker"):
+ with patch("tools.environments.utils.shutil.which", return_value="/usr/local/bin/docker"):
first = docker_mod.find_docker()
# Second call should use cache, not call shutil.which again
- with patch("tools.environments.docker.shutil.which", return_value=None):
+ with patch("tools.environments.utils.shutil.which", return_value=None):
second = docker_mod.find_docker()
assert first == second == "/usr/local/bin/docker"
diff --git a/tools/approval.py b/tools/approval.py
index faf888f184e8d..ac3dce644760b 100644
--- a/tools/approval.py
+++ b/tools/approval.py
@@ -582,13 +582,13 @@ def check_dangerous_command(command: str, env_type: str,
Args:
command: The shell command to check.
- env_type: Terminal backend type ('local', 'ssh', 'docker', etc.).
+ env_type: Terminal backend type ('local', 'ssh', 'docker', 'podman', etc.).
approval_callback: Optional CLI callback for interactive prompts.
Returns:
{"approved": True/False, "message": str or None, ...}
"""
- if env_type in ("docker", "singularity", "modal", "daytona"):
+ if env_type in ("docker", "podman", "singularity", "modal", "daytona"):
return {"approved": True, "message": None}
# --yolo: bypass all approval prompts. Gateway /yolo is session-scoped;
@@ -690,7 +690,7 @@ def check_all_command_guards(command: str, env_type: str,
other was shown to the user.
"""
# Skip containers for both checks
- if env_type in ("docker", "singularity", "modal", "daytona"):
+ if env_type in ("docker", "podman", "singularity", "modal", "daytona"):
return {"approved": True, "message": None}
# --yolo or approvals.mode=off: bypass all approval prompts.
diff --git a/tools/code_execution_tool.py b/tools/code_execution_tool.py
index 93863efe994e6..efa5f36892fe4 100644
--- a/tools/code_execution_tool.py
+++ b/tools/code_execution_tool.py
@@ -467,6 +467,8 @@ def _get_or_create_env(task_id: str):
if env_type == "docker":
image = overrides.get("docker_image") or config["docker_image"]
+ elif env_type == "podman":
+ image = overrides.get("podman_image") or config["podman_image"]
elif env_type == "singularity":
image = overrides.get("singularity_image") or config["singularity_image"]
elif env_type == "modal":
@@ -479,7 +481,7 @@ def _get_or_create_env(task_id: str):
cwd = overrides.get("cwd") or config["cwd"]
container_config = None
- if env_type in ("docker", "singularity", "modal", "daytona"):
+ if env_type in ("docker", "podman", "singularity", "modal", "daytona"):
container_config = {
"container_cpu": config.get("container_cpu", 1),
"container_memory": config.get("container_memory", 5120),
@@ -488,6 +490,29 @@ def _get_or_create_env(task_id: str):
"docker_volumes": config.get("docker_volumes", []),
}
+ if env_type == "podman":
+ podman_user = config.get("podman_user", "")
+ podman_userns = config.get("podman_userns", "")
+ podman_extra_args = config.get("podman_extra_args", [])
+ podman_extra_capabilities = config.get("podman_extra_capabilities", [])
+ podman_privilged = config.get("podman_privileged", False)
+ podman_rootful = config.get("podman_rootful", False)
+
+ if str(podman_user).strip():
+ container_config["podman_user"] = podman_user
+
+ if str(podman_userns).strip():
+ container_config["podman_userns"] = podman_userns
+
+ if isinstance(podman_extra_args, list) and all(podman_extra_args, lambda x: isinstance(x, str)):
+ container_config["podman_extra_args"] = podman_extra_args
+
+ if isinstance(podman_extra_capabilities, list) and all(podman_extra_capabilities, lambda x: isinstance(x, str)):
+ container_config["podman_extra_capabilities"] = podman_extra_capabilities
+
+ container_config["podman_privileged"] = podman_privilged
+ container_config["podman_rootful"] = podman_rootful
+
ssh_config = None
if env_type == "ssh":
ssh_config = {
diff --git a/tools/environments/docker.py b/tools/environments/docker.py
index 2341778f4cb09..3e8fee3fb0b5a 100644
--- a/tools/environments/docker.py
+++ b/tools/environments/docker.py
@@ -15,6 +15,11 @@
from typing import Optional
from tools.environments.base import BaseEnvironment, _popen_bash
+from tools.environments.utils import \
+ normalize_forward_env_names, \
+ normalize_env_dict, \
+ load_hermes_env_vars, \
+ find_container_cli_binary
from tools.environments.local import _HERMES_PROVIDER_ENV_BLOCKLIST
logger = logging.getLogger(__name__)
@@ -30,98 +35,19 @@
]
_docker_executable: Optional[str] = None # resolved once, cached
-_ENV_VAR_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
-
-
-def _normalize_forward_env_names(forward_env: list[str] | None) -> list[str]:
- """Return a deduplicated list of valid environment variable names."""
- normalized: list[str] = []
- seen: set[str] = set()
-
- for item in forward_env or []:
- if not isinstance(item, str):
- logger.warning("Ignoring non-string docker_forward_env entry: %r", item)
- continue
-
- key = item.strip()
- if not key:
- continue
- if not _ENV_VAR_NAME_RE.match(key):
- logger.warning("Ignoring invalid docker_forward_env entry: %r", item)
- continue
- if key in seen:
- continue
-
- seen.add(key)
- normalized.append(key)
-
- return normalized
-
-
-def _normalize_env_dict(env: dict | None) -> dict[str, str]:
- """Validate and normalize a docker_env dict to {str: str}.
-
- Filters out entries with invalid variable names or non-string values.
- """
- if not env:
- return {}
- if not isinstance(env, dict):
- logger.warning("docker_env is not a dict: %r", env)
- return {}
-
- normalized: dict[str, str] = {}
- for key, value in env.items():
- if not isinstance(key, str) or not _ENV_VAR_NAME_RE.match(key.strip()):
- logger.warning("Ignoring invalid docker_env key: %r", key)
- continue
- key = key.strip()
- if not isinstance(value, str):
- # Coerce simple scalar types (int, bool, float) to string;
- # reject complex types.
- if isinstance(value, (int, float, bool)):
- value = str(value)
- else:
- logger.warning("Ignoring non-string docker_env value for %r: %r", key, value)
- continue
- normalized[key] = value
-
- return normalized
-
-
-def _load_hermes_env_vars() -> dict[str, str]:
- """Load ~/.hermes/.env values without failing Docker command execution."""
- try:
- from hermes_cli.config import load_env
-
- return load_env() or {}
- except Exception:
- return {}
def find_docker() -> Optional[str]:
- """Locate the docker CLI binary.
-
- Checks ``shutil.which`` first (respects PATH), then probes well-known
- install locations on macOS where Docker Desktop may not be in PATH
- (e.g. when running as a gateway service via launchd).
-
- Returns the absolute path, or ``None`` if docker cannot be found.
- """
+ """Locate the docker CLI binary"""
global _docker_executable
if _docker_executable is not None:
return _docker_executable
- found = shutil.which("docker")
+ found = find_container_cli_binary("docker", _DOCKER_SEARCH_PATHS)
if found:
_docker_executable = found
return found
- for path in _DOCKER_SEARCH_PATHS:
- if os.path.isfile(path) and os.access(path, os.X_OK):
- _docker_executable = path
- logger.info("Found docker at non-PATH location: %s", path)
- return path
-
return None
@@ -248,8 +174,8 @@ def __init__(
super().__init__(cwd=cwd, timeout=timeout)
self._persistent = persistent_filesystem
self._task_id = task_id
- self._forward_env = _normalize_forward_env_names(forward_env)
- self._env = _normalize_env_dict(env)
+ self._forward_env = normalize_forward_env_names(forward_env)
+ self._env = normalize_env_dict(env)
self._container_id: Optional[str] = None
logger.info(f"DockerEnvironment volumes: {volumes}")
# Ensure volumes is a list (config.yaml could be malformed)
@@ -454,7 +380,7 @@ def _build_init_env_args(self) -> list[str]:
# win over the generic Hermes secret blocklist. Only implicit passthrough
# keys are filtered.
forward_keys = explicit_forward_keys | (passthrough_keys - _HERMES_PROVIDER_ENV_BLOCKLIST)
- hermes_env = _load_hermes_env_vars() if forward_keys else {}
+ hermes_env = load_hermes_env_vars() if forward_keys else {}
for key in sorted(forward_keys):
value = os.getenv(key)
if value is None:
diff --git a/tools/environments/podman.py b/tools/environments/podman.py
new file mode 100644
index 0000000000000..ddbb7fdaeaf16
--- /dev/null
+++ b/tools/environments/podman.py
@@ -0,0 +1,442 @@
+"""Podman execution environment for sandboxed command execution.
+
+Security hardened (cap-drop ALL, no-new-privileges, PID limits),
+optionally rootless and user-namespace-remapped
+configurable resource limits (CPU, memory, disk), and optional filesystem
+persistence via bind mounts.
+"""
+
+import logging
+import os
+import re
+import shutil
+import subprocess
+import sys
+import uuid
+from typing import Optional
+
+from tools.environments.base import _popen_bash
+# for now, we'll use the exact same default security args as the Docker backend
+from tools.environments.docker import DockerEnvironment, _SECURITY_ARGS
+from tools.environments.utils import \
+ normalize_forward_env_names, \
+ normalize_env_dict, \
+ load_hermes_env_vars, \
+ find_container_cli_binary
+from tools.environments.local import _HERMES_PROVIDER_ENV_BLOCKLIST
+
+logger = logging.getLogger(__name__)
+
+
+# Common Podman install paths checked when 'podman' is not in PATH.
+_PODMAN_SEARCH_PATHS = [
+ "/usr/bin/podman"
+ "/usr/local/bin/podman",
+ "/opt/homebrew/bin/podman",
+ "/opt/podman/bin/podman",
+ "/home/linuxbrew/.linuxbrew/bin/podman",
+]
+
+_podman_executable: Optional[str] = None # resolved once, cached
+
+
+def find_podman() -> Optional[str]:
+ """Locate the podman CLI binary"""
+ global _podman_executable
+ if _podman_executable is not None:
+ return _podman_executable
+
+ found = find_container_cli_binary("podman", _PODMAN_SEARCH_PATHS)
+ if found:
+ _podman_executable = found
+ return found
+
+ return None
+
+
+def _ensure_podman_available() -> None:
+ """Best-effort check that the podman CLI is available before use.
+
+ Reuses ``find_podman()`` so this preflight stays consistent with the rest of
+ the Podman backend, including known non-PATH Podman Desktop locations.
+ """
+ podman_exe = find_podman()
+ if not podman_exe:
+ logger.error(
+ "Podman backend selected but no podman executable was found in PATH "
+ "or known install locations. Install Podman Desktop and ensure the "
+ "CLI is available."
+ )
+ raise RuntimeError(
+ "Podman executable not found in PATH or known install locations. "
+ "Install Podman and ensure the 'podman' command is available."
+ )
+
+ try:
+ result = subprocess.run(
+ [podman_exe, "version"],
+ capture_output=True,
+ text=True,
+ timeout=5,
+ )
+ except FileNotFoundError:
+ logger.error(
+ "Podman backend selected but the resolved podman executable '%s' could "
+ "not be executed.",
+ podman_exe,
+ exc_info=True,
+ )
+ raise RuntimeError(
+ "Podman executable could not be executed. Check your Podman installation."
+ )
+ except subprocess.TimeoutExpired:
+ logger.error(
+ "Podman backend selected but '%s version' timed out.",
+ podman_exe,
+ exc_info=True,
+ )
+ raise RuntimeError(
+ "`podman version` is not responding."
+ )
+ except Exception:
+ logger.error(
+ "Unexpected error while checking Podman availability.",
+ exc_info=True,
+ )
+ raise
+ else:
+ if result.returncode != 0:
+ logger.error(
+ "Podman backend selected but '%s version' failed "
+ "(exit code %d, stderr=%s)",
+ podman_exe,
+ result.returncode,
+ result.stderr.strip(),
+ )
+ raise RuntimeError(
+ "Podman command is available but 'podman version' failed. "
+ "Check your Podman installation."
+ )
+
+
+class PodmanEnvironment(DockerEnvironment):
+ """Hardened Podman container execution with resource limits and persistence.
+
+ Security: all capabilities dropped, no privilege escalation, PID limits,
+ size-limited tmpfs for scratch dirs. The container itself is the security
+ boundary — the filesystem inside is writable so agents can install packages
+ (pip, npm, apt) as needed. Writable workspace via tmpfs or bind mounts.
+
+ Persistence: when enabled, bind mounts preserve /workspace and /root
+ across container restarts.
+
+ This class is derived from DockerEnvironment so we can reuse the
+ _build_init_env_args method. Meanwhile, we support some options that are
+ first-class only in Podman, in particular:
+ 1) rootless vs rootful
+ 2) privileged mode
+ """
+
+ def __init__(
+ self,
+ image: str,
+ cwd: str = "/root",
+ timeout: int = 60,
+ cpu: float = 0,
+ memory: int = 0,
+ disk: int = 0,
+ persistent_filesystem: bool = False,
+ task_id: str = "default",
+ volumes: list = None,
+ forward_env: list[str] | None = None,
+ env: dict | None = None,
+ network: bool = True,
+ host_cwd: str = None,
+ auto_mount_cwd: bool = False,
+ # New Podman-specific options
+ userns: str = "",
+ user: str = "",
+ privileged: bool = False,
+ extra_capabilities: list = None,
+ extra_args: list = None,
+ rootful: bool = False,
+ ):
+ if cwd == "~":
+ cwd = "/root"
+ super().__init__(cwd=cwd, timeout=timeout)
+ self._persistent = persistent_filesystem
+ self._task_id = task_id
+ self._forward_env = normalize_forward_env_names(forward_env)
+ self._env = normalize_env_dict(env)
+ self._container_id: Optional[str] = None
+ # Podman-specific options
+ self._privileged = privileged
+ self._userns = userns
+ self._user = user
+ self._extra_capabilities = extra_capabilities or []
+ self._extra_args = extra_args or []
+ self._rootful = rootful
+ logger.info(f"PodmanEnvironment volumes: {volumes}")
+ # Ensure volumes is a list (config.yaml could be malformed)
+ if volumes is not None and not isinstance(volumes, list):
+ logger.warning(f"docker_volumes config is not a list: {volumes!r}")
+ volumes = []
+
+ # Fail fast if Podman is not available.
+ _ensure_podman_available()
+
+ # Build resource limit args
+ resource_args = []
+ if cpu > 0:
+ resource_args.extend(["--cpus", str(cpu)])
+ if memory > 0:
+ resource_args.extend(["--memory", f"{memory}m"])
+ if disk > 0:
+ logger.warning(
+ "Podman storage driver does not support per-container disk limits. "
+ "Container will run without disk quota."
+ )
+ if not network:
+ resource_args.append("--network=none")
+
+ # Persistent workspace via bind mounts from a configurable host directory
+ # (TERMINAL_SANDBOX_DIR, default ~/.hermes/sandboxes/). Non-persistent
+ # mode uses tmpfs (ephemeral, fast, gone on cleanup).
+ from tools.environments.base import get_sandbox_dir
+
+ # User-configured volume mounts (from config.yaml docker_volumes)
+ volume_args = []
+ workspace_explicitly_mounted = False
+ for vol in (volumes or []):
+ if not isinstance(vol, str):
+ logger.warning(f"Podman volume entry is not a string: {vol!r}")
+ continue
+ vol = vol.strip()
+ if not vol:
+ continue
+ if ":" in vol:
+ volume_args.extend(["-v", vol])
+ if ":/workspace" in vol:
+ workspace_explicitly_mounted = True
+ else:
+ logger.warning(f"Podman volume '{vol}' missing colon, skipping")
+
+ host_cwd_abs = os.path.abspath(os.path.expanduser(host_cwd)) if host_cwd else ""
+ bind_host_cwd = (
+ auto_mount_cwd
+ and bool(host_cwd_abs)
+ and os.path.isdir(host_cwd_abs)
+ and not workspace_explicitly_mounted
+ )
+ if auto_mount_cwd and host_cwd and not os.path.isdir(host_cwd_abs):
+ logger.debug(f"Skipping podman cwd mount: host_cwd is not a valid directory: {host_cwd}")
+
+ self._workspace_dir: Optional[str] = None
+ self._home_dir: Optional[str] = None
+ writable_args = []
+ if self._persistent:
+ sandbox = get_sandbox_dir() / "podman" / task_id
+ self._home_dir = str(sandbox / "home")
+ os.makedirs(self._home_dir, exist_ok=True)
+ writable_args.extend([
+ "-v", f"{self._home_dir}:/root",
+ ])
+ if not bind_host_cwd and not workspace_explicitly_mounted:
+ self._workspace_dir = str(sandbox / "workspace")
+ os.makedirs(self._workspace_dir, exist_ok=True)
+ writable_args.extend([
+ "-v", f"{self._workspace_dir}:/workspace",
+ ])
+ else:
+ if not bind_host_cwd and not workspace_explicitly_mounted:
+ writable_args.extend([
+ "--tmpfs", "/workspace:rw,exec,size=10g",
+ ])
+ writable_args.extend([
+ "--tmpfs", "/home:rw,exec,size=1g",
+ "--tmpfs", "/root:rw,exec,size=1g",
+ ])
+
+ if bind_host_cwd:
+ logger.info(f"Mounting configured host cwd to /workspace: {host_cwd_abs}")
+ volume_args = ["-v", f"{host_cwd_abs}:/workspace", *volume_args]
+ elif workspace_explicitly_mounted:
+ logger.debug("Skipping podman cwd mount: /workspace already mounted by user config")
+
+ # Mount credential files (OAuth tokens, etc.) declared by skills.
+ # Read-only so the container can authenticate but not modify host creds.
+ try:
+ from tools.credential_files import (
+ get_credential_file_mounts,
+ get_skills_directory_mount,
+ get_cache_directory_mounts,
+ )
+
+ for mount_entry in get_credential_file_mounts():
+ volume_args.extend([
+ "-v",
+ f"{mount_entry['host_path']}:{mount_entry['container_path']}:ro",
+ ])
+ logger.info(
+ "Podman: mounting credential %s -> %s",
+ mount_entry["host_path"],
+ mount_entry["container_path"],
+ )
+
+ # Mount skill directories (local + external) so skill
+ # scripts/templates are available inside the container.
+ for skills_mount in get_skills_directory_mount():
+ volume_args.extend([
+ "-v",
+ f"{skills_mount['host_path']}:{skills_mount['container_path']}:ro",
+ ])
+ logger.info(
+ "Podman: mounting skills dir %s -> %s",
+ skills_mount["host_path"],
+ skills_mount["container_path"],
+ )
+
+ # Mount host-side cache directories (documents, images, audio,
+ # screenshots) so the agent can access uploaded files and other
+ # cached media from inside the container. Read-only — the
+ # container reads these but the host gateway manages writes.
+ for cache_mount in get_cache_directory_mounts():
+ volume_args.extend([
+ "-v",
+ f"{cache_mount['host_path']}:{cache_mount['container_path']}:ro",
+ ])
+ logger.info(
+ "Podman: mounting cache dir %s -> %s",
+ cache_mount["host_path"],
+ cache_mount["container_path"],
+ )
+ except Exception as e:
+ logger.debug("Podman: could not load credential file mounts: %s", e)
+
+ # Apply privileged flag
+ if self._privileged:
+ writable_args.append("--privileged")
+
+ # Apply user namespace
+ if self._userns:
+ writable_args.extend(["--userns", self._userns])
+
+ # Apply user
+ if self._user:
+ writable_args.extend(["--user", self._user])
+
+ # Apply extra capabilities (additive to defaults)
+ if self._extra_capabilities:
+ for cap in self._extra_capabilities:
+ writable_args.extend(["--cap-add", cap])
+
+ # Apply extra args (no validation)
+ if self._extra_args:
+ writable_args.extend(self._extra_args)
+
+ # Explicit environment variables (docker_env config) — set at container
+ # creation so they're available to all processes (including entrypoint).
+ env_args = []
+ for key in sorted(self._env):
+ env_args.extend(["-e", f"{key}={self._env[key]}"])
+
+ logger.info(f"Podman volume_args: {volume_args}")
+ all_run_args = list(_SECURITY_ARGS) + writable_args + resource_args + volume_args + env_args
+ logger.info(f"Podman run_args: {all_run_args}")
+
+ # Resolve the podman executable once so it works even when
+ # /usr/local/bin is not in PATH (common on macOS gateway/service).
+ self._podman_exe = find_podman() or "podman"
+
+ # Start the container directly via `podman run -d`.
+ container_name = f"hermes-{uuid.uuid4().hex[:8]}"
+ run_cmd = [
+ self._podman_exe, "run", "-d",
+ "--name", container_name,
+ "-w", cwd,
+ *all_run_args,
+ image,
+ "sleep", "2h",
+ ]
+ if self._rootful:
+ run_cmd = ["sudo"] + run_cmd
+ logger.debug(f"Starting container: {' '.join(run_cmd)}")
+ result = subprocess.run(
+ run_cmd,
+ capture_output=True,
+ text=True,
+ timeout=120, # image pull may take a while
+ check=True,
+ )
+ self._container_id = result.stdout.strip()
+ logger.info(f"Started container {container_name} ({self._container_id[:12]})")
+
+ # Build init-time env forwarding args (used only by init_session
+ # to inject host env vars into the snapshot; subsequent commands get
+ # them from the snapshot file).
+ self._init_env_args = self._build_init_env_args()
+
+ # Initialize session snapshot inside container
+ self.init_session()
+
+ def _run_bash(self, cmd_string: str, *, login: bool = False,
+ timeout: int = 120,
+ stdin_data: str | None = None) -> subprocess.Popen:
+ """Spawn a bash process inside Podman container."""
+ assert self._container_id, "Container not started"
+ cmd = [self._podman_exe, "exec"]
+
+ # Rootful support: prefix with sudo if needed
+ if self._rootful:
+ cmd = ["sudo"] + cmd
+
+ if stdin_data is not None:
+ cmd.append("-i")
+
+ # Only inject -e env args during init_session (login=True).
+ # Subsequent commands get env vars from the snapshot file.
+ if login:
+ cmd.extend(self._init_env_args)
+
+ cmd.extend([self._container_id])
+
+ if login:
+ cmd.extend(["bash", "-l", "-c", cmd_string])
+ else:
+ cmd.extend(["bash", "-c", cmd_string])
+
+ return _popen_bash(cmd, stdin_data)
+
+ def cleanup(self):
+ """Stop and remove the container. Bind-mount dirs persist if persistent=True.
+
+ Our implementation needs to support rootful mode.
+ """
+ if self._container_id:
+ try:
+ # Stop in background so cleanup doesn't block
+ sudo_prefix = "sudo " if self._rootful else ""
+ stop_cmd = (
+ f"(timeout 60 {sudo_prefix}{self._podman_exe} stop {self._container_id} || "
+ f"{sudo_prefix}{self._podman_exe} rm -f {self._container_id}) >/dev/null 2>&1 &"
+ )
+ subprocess.Popen(stop_cmd, shell=True)
+ except Exception as e:
+ logger.warning("Failed to stop container %s: %s", self._container_id, e)
+
+ if not self._persistent:
+ # Also schedule removal (stop only leaves it as stopped)
+ sudo_prefix = "sudo " if self._rootful else ""
+ try:
+ subprocess.Popen(
+ f"sleep 3 && {sudo_prefix}{self._podman_exe} rm -f {self._container_id} >/dev/null 2>&1 &",
+ shell=True,
+ )
+ except Exception:
+ pass
+ self._container_id = None
+
+ if not self._persistent:
+ for d in (self._workspace_dir, self._home_dir):
+ if d:
+ shutil.rmtree(d, ignore_errors=True)
diff --git a/tools/environments/utils.py b/tools/environments/utils.py
new file mode 100644
index 0000000000000..26562c2636623
--- /dev/null
+++ b/tools/environments/utils.py
@@ -0,0 +1,94 @@
+"""Utility functions that are reusable among certain terminal backend implementations."""
+
+import logging
+import os
+import re
+import shutil
+
+logger = logging.getLogger(__name__)
+
+_ENV_VAR_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
+
+def normalize_forward_env_names(forward_env: list[str] | None) -> list[str]:
+ """Return a deduplicated list of valid environment variable names."""
+ normalized: list[str] = []
+ seen: set[str] = set()
+
+ for item in forward_env or []:
+ if not isinstance(item, str):
+ logger.warning("Ignoring non-string docker_forward_env entry: %r", item)
+ continue
+
+ key = item.strip()
+ if not key:
+ continue
+ if not _ENV_VAR_NAME_RE.match(key):
+ logger.warning("Ignoring invalid docker_forward_env entry: %r", item)
+ continue
+ if key in seen:
+ continue
+
+ seen.add(key)
+ normalized.append(key)
+
+ return normalized
+
+
+def normalize_env_dict(env: dict | None) -> dict[str, str]:
+ """Validate and normalize a docker_env dict to {str: str}.
+
+ Filters out entries with invalid variable names or non-string values.
+ """
+ if not env:
+ return {}
+ if not isinstance(env, dict):
+ logger.warning("docker_env is not a dict: %r", env)
+ return {}
+
+ normalized: dict[str, str] = {}
+ for key, value in env.items():
+ if not isinstance(key, str) or not _ENV_VAR_NAME_RE.match(key.strip()):
+ logger.warning("Ignoring invalid docker_env key: %r", key)
+ continue
+ key = key.strip()
+ if not isinstance(value, str):
+ # Coerce simple scalar types (int, bool, float) to string;
+ # reject complex types.
+ if isinstance(value, (int, float, bool)):
+ value = str(value)
+ else:
+ logger.warning("Ignoring non-string docker_env value for %r: %r", key, value)
+ continue
+ normalized[key] = value
+
+ return normalized
+
+
+def load_hermes_env_vars() -> dict[str, str]:
+ """Load ~/.hermes/.env values without failing Docker/Podman command execution."""
+ try:
+ from hermes_cli.config import load_env
+
+ return load_env() or {}
+ except Exception:
+ return {}
+
+
+def find_container_cli_binary(exec_name: str, search_paths: list[str]):
+ """Locate the container CLI binary.
+
+ Checks ``shutil.which`` first (respects PATH), then probes well-known
+ install locations where docker or podman may be found.
+
+ Returns the absolute path, or ``None`` if docker or podman cannot be found.
+ """
+ found = shutil.which(exec_name)
+ if found:
+ return found
+
+ for path in search_paths:
+ if os.path.isfile(path) and os.access(path, os.X_OK):
+ logger.info("Found %s at non-PATH location: %s", exec_name, path)
+ return path
+
+ return None
diff --git a/tools/file_operations.py b/tools/file_operations.py
index 29180931dc5e7..df244cd0db7f9 100644
--- a/tools/file_operations.py
+++ b/tools/file_operations.py
@@ -3,7 +3,7 @@
File Operations Module
Provides file manipulation capabilities (read, write, patch, search) that work
-across all terminal backends (local, docker, singularity, ssh, modal, daytona).
+across all terminal backends (local, docker, podman, singularity, ssh, modal, daytona).
The key insight is that all file operations can be expressed as shell commands,
so we wrap the terminal backend's execute() interface to provide a unified file API.
@@ -324,7 +324,7 @@ class ShellFileOperations(FileOperations):
File operations implemented via shell commands.
Works with ANY terminal backend that has execute(command, cwd) method.
- This includes local, docker, singularity, ssh, modal, and daytona environments.
+ This includes local, docker, podman, singularity, ssh, modal, and daytona environments.
"""
def __init__(self, terminal_env, cwd: str = None):
@@ -339,7 +339,7 @@ def __init__(self, terminal_env, cwd: str = None):
self.env = terminal_env
# Determine cwd from various possible sources.
# IMPORTANT: do NOT fall back to os.getcwd() -- that's the HOST's local
- # path which doesn't exist inside container/cloud backends (modal, docker).
+ # path which doesn't exist inside container/cloud backends (modal, docker, podman).
# If nothing provides a cwd, use "/" as a safe universal default.
self.cwd = cwd or getattr(terminal_env, 'cwd', None) or \
getattr(getattr(terminal_env, 'config', None), 'cwd', None) or "/"
diff --git a/tools/file_tools.py b/tools/file_tools.py
index 186a9d052c6f4..0dcf27e936fe4 100644
--- a/tools/file_tools.py
+++ b/tools/file_tools.py
@@ -204,6 +204,8 @@ def _get_file_ops(task_id: str = "default") -> ShellFileOperations:
if env_type == "docker":
image = overrides.get("docker_image") or config["docker_image"]
+ elif env_type == "podman":
+ image = overrides.get("podman_image") or config["podman_image"]
elif env_type == "singularity":
image = overrides.get("singularity_image") or config["singularity_image"]
elif env_type == "modal":
@@ -217,7 +219,7 @@ def _get_file_ops(task_id: str = "default") -> ShellFileOperations:
logger.info("Creating new %s environment for task %s...", env_type, task_id[:8])
container_config = None
- if env_type in ("docker", "singularity", "modal", "daytona"):
+ if env_type in ("docker", "podman", "singularity", "modal", "daytona"):
container_config = {
"container_cpu": config.get("container_cpu", 1),
"container_memory": config.get("container_memory", 5120),
@@ -226,6 +228,29 @@ def _get_file_ops(task_id: str = "default") -> ShellFileOperations:
"docker_volumes": config.get("docker_volumes", []),
}
+ if env_type == "podman":
+ podman_user = config.get("podman_user", "")
+ podman_userns = config.get("podman_userns", "")
+ podman_extra_args = config.get("podman_extra_args", [])
+ podman_extra_capabilities = config.get("podman_extra_capabilities", [])
+ podman_privilged = config.get("podman_privileged", False)
+ podman_rootful = config.get("podman_rootful", False)
+
+ if str(podman_user).strip():
+ container_config["podman_user"] = podman_user
+
+ if str(podman_userns).strip():
+ container_config["podman_userns"] = podman_userns
+
+ if isinstance(podman_extra_args, list) and all(podman_extra_args, lambda x: isinstance(x, str)):
+ container_config["podman_extra_args"] = podman_extra_args
+
+ if isinstance(podman_extra_capabilities, list) and all(podman_extra_capabilities, lambda x: isinstance(x, str)):
+ container_config["podman_extra_capabilities"] = podman_extra_capabilities
+
+ container_config["podman_privileged"] = podman_privilged
+ container_config["podman_rootful"] = podman_rootful
+
ssh_config = None
if env_type == "ssh":
ssh_config = {
diff --git a/tools/skills_tool.py b/tools/skills_tool.py
index 085ed0055094d..987d5795af6c0 100644
--- a/tools/skills_tool.py
+++ b/tools/skills_tool.py
@@ -100,7 +100,7 @@
}
_ENV_VAR_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
_EXCLUDED_SKILL_DIRS = frozenset((".git", ".github", ".hub"))
-_REMOTE_ENV_BACKENDS = frozenset({"docker", "singularity", "modal", "ssh", "daytona"})
+_REMOTE_ENV_BACKENDS = frozenset({"docker", "podman", "singularity", "modal", "ssh", "daytona"})
_secret_capture_callback = None
diff --git a/tools/terminal_tool.py b/tools/terminal_tool.py
index 42415a5f14a3a..10a315e2a2858 100644
--- a/tools/terminal_tool.py
+++ b/tools/terminal_tool.py
@@ -8,10 +8,11 @@
Environment Selection (via TERMINAL_ENV environment variable):
- "local": Execute directly on the host machine (default, fastest)
- "docker": Execute in Docker containers (isolated, requires Docker)
+- "podman": Execute in Podman containers (isolated, requires Podman)
- "modal": Execute in Modal cloud sandboxes (direct Modal or managed gateway)
Features:
-- Multiple execution backends (local, docker, modal)
+- Multiple execution backends (local, docker, podman, modal)
- Background task support
- VM/container lifecycle management
- Automatic cleanup after inactivity
@@ -465,9 +466,9 @@ def _transform_sudo_command(command: str | None) -> tuple[str | None, str | None
returned unchanged so it fails gracefully with
"sudo: a password is required".
- Callers that drive a subprocess directly (local, ssh, docker, singularity)
- should prepend sudo_stdin to their stdin_data and pass the merged bytes to
- Popen's stdin pipe.
+ Callers that drive a subprocess directly (local, ssh, docker, podman,
+ singularity) should prepend sudo_stdin to their stdin_data and pass the
+ merged bytes to Popen's stdin pipe.
Callers that cannot pipe subprocess stdin (modal, daytona) must embed the
password in the command string themselves; see their execute() methods for
@@ -507,6 +508,7 @@ def _transform_sudo_command(command: str | None) -> tuple[str | None, str | None
from tools.environments.singularity import SingularityEnvironment as _SingularityEnvironment
from tools.environments.ssh import SSHEnvironment as _SSHEnvironment
from tools.environments.docker import DockerEnvironment as _DockerEnvironment
+from tools.environments.podman import PodmanEnvironment as _PodmanEnvironment
from tools.environments.modal import ModalEnvironment as _ModalEnvironment
from tools.environments.managed_modal import ManagedModalEnvironment as _ManagedModalEnvironment
from tools.managed_tool_gateway import is_managed_tool_gateway_ready
@@ -624,7 +626,7 @@ def _get_env_config() -> Dict[str, Any]:
cwd = os.getenv("TERMINAL_CWD", default_cwd)
host_cwd = None
host_prefixes = ("/Users/", "/home/", "C:\\", "C:/")
- if env_type == "docker" and mount_docker_cwd:
+ if env_type in ("docker", "podman") and mount_docker_cwd:
docker_cwd_source = os.getenv("TERMINAL_CWD") or os.getcwd()
candidate = os.path.abspath(os.path.expanduser(docker_cwd_source))
if (
@@ -633,7 +635,7 @@ def _get_env_config() -> Dict[str, Any]:
):
host_cwd = candidate
cwd = "/workspace"
- elif env_type in ("modal", "docker", "singularity", "daytona") and cwd:
+ elif env_type in ("modal", "docker", "podman", "singularity", "daytona") and cwd:
# Host paths and relative paths that won't work inside containers
is_host_path = any(cwd.startswith(p) for p in host_prefixes)
is_relative = not os.path.isabs(cwd) # e.g. "." or "src/"
@@ -647,6 +649,7 @@ def _get_env_config() -> Dict[str, Any]:
"env_type": env_type,
"modal_mode": coerce_modal_mode(os.getenv("TERMINAL_MODAL_MODE", "auto")),
"docker_image": os.getenv("TERMINAL_DOCKER_IMAGE", default_image),
+ "podman_image": os.getenv("TERMINAL_PODMAN_IMAGE", f"docker.io/{default_image}"),
"docker_forward_env": _parse_env_var("TERMINAL_DOCKER_FORWARD_ENV", "[]", json.loads, "valid JSON"),
"singularity_image": os.getenv("TERMINAL_SINGULARITY_IMAGE", f"docker://{default_image}"),
"modal_image": os.getenv("TERMINAL_MODAL_IMAGE", default_image),
@@ -669,12 +672,19 @@ def _get_env_config() -> Dict[str, Any]:
os.getenv("TERMINAL_PERSISTENT_SHELL", "true"),
).lower() in ("true", "1", "yes"),
"local_persistent": os.getenv("TERMINAL_LOCAL_PERSISTENT", "false").lower() in ("true", "1", "yes"),
- # Container resource config (applies to docker, singularity, modal, daytona -- ignored for local/ssh)
+ # Container resource config (applies to docker, podman, singularity, modal, daytona -- ignored for local/ssh)
"container_cpu": _parse_env_var("TERMINAL_CONTAINER_CPU", "1", float, "number"),
"container_memory": _parse_env_var("TERMINAL_CONTAINER_MEMORY", "5120"), # MB (default 5GB)
"container_disk": _parse_env_var("TERMINAL_CONTAINER_DISK", "51200"), # MB (default 50GB)
"container_persistent": os.getenv("TERMINAL_CONTAINER_PERSISTENT", "true").lower() in ("true", "1", "yes"),
"docker_volumes": _parse_env_var("TERMINAL_DOCKER_VOLUMES", "[]", json.loads, "valid JSON"),
+ # Podman-specific config
+ "podman_userns": os.getenv("TERMINAL_PODMAN_USERNS", ""),
+ "podman_user": os.getenv("TERMINAL_PODMAN_USER", ""),
+ "podman_privileged": os.getenv("TERMINAL_PODMAN_PRIVILEGED", "false").lower() in ("true", "1", "yes"),
+ "podman_extra_capabilities": _parse_env_var("TERMINAL_PODMAN_EXTRA_CAPABILITIES", "[]", json.loads, "valid JSON"),
+ "podman_extra_args": _parse_env_var("TERMINAL_PODMAN_EXTRA_ARGS", "[]", json.loads, "valid JSON"),
+ "podman_rootful": os.getenv("TERMINAL_PODMAN_ROOTFUL", "false").lower() in ("true", "1", "yes"),
}
@@ -696,14 +706,14 @@ def _create_environment(env_type: str, image: str, cwd: str, timeout: int,
Create an execution environment for sandboxed command execution.
Args:
- env_type: One of "local", "docker", "singularity", "modal", "daytona", "ssh"
- image: Docker/Singularity/Modal image name (ignored for local/ssh)
+ env_type: One of "local", "docker", "podman", "singularity", "modal", "daytona", "ssh"
+ image: Docker/Podman/Singularity/Modal image name (ignored for local/ssh)
cwd: Working directory
timeout: Default command timeout
ssh_config: SSH connection config (for env_type="ssh")
container_config: Resource config for container backends (cpu, memory, disk, persistent)
task_id: Task identifier for environment reuse and snapshot keying
- host_cwd: Optional host working directory to bind into Docker when explicitly enabled
+ host_cwd: Optional host working directory to bind into Docker/Podman when explicitly enabled
Returns:
Environment instance with execute() method
@@ -738,6 +748,26 @@ def _create_environment(env_type: str, image: str, cwd: str, timeout: int,
cpu=cpu, memory=memory, disk=disk,
persistent_filesystem=persistent, task_id=task_id,
)
+
+ elif env_type == "podman":
+ return _PodmanEnvironment(
+ image=image, cwd=cwd, timeout=timeout,
+ cpu=cpu, memory=memory, disk=disk,
+ persistent_filesystem=persistent, task_id=task_id,
+ volumes=volumes,
+ host_cwd=host_cwd,
+ auto_mount_cwd=cc.get("docker_mount_cwd_to_workspace", False),
+ forward_env=docker_forward_env,
+ env=docker_env,
+ network=cc.get("network", True),
+ # Podman-specific options
+ userns=cc.get("podman_userns", ""),
+ user=cc.get("podman_user", ""),
+ privileged=cc.get("podman_privileged", False),
+ extra_capabilities=cc.get("podman_extra_capabilities", []),
+ extra_args=cc.get("podman_extra_args", []),
+ rootful=cc.get("podman_rootful", False),
+ )
elif env_type == "modal":
sandbox_kwargs = {}
@@ -813,7 +843,7 @@ def _create_environment(env_type: str, image: str, cwd: str, timeout: int,
)
else:
- raise ValueError(f"Unknown environment type: {env_type}. Use 'local', 'docker', 'singularity', 'modal', 'daytona', or 'ssh'")
+ raise ValueError(f"Unknown environment type: {env_type}. Use 'local', 'docker', 'podman', 'singularity', 'modal', 'daytona', or 'ssh'")
def _cleanup_inactive_envs(lifetime_seconds: int = 300):
@@ -926,7 +956,7 @@ def is_persistent_env(task_id: str) -> bool:
cross-turn persistence (``persistent_filesystem=True``).
Used by the agent loop to skip per-turn teardown for backends whose whole
- point is to survive between turns (docker with ``container_persistent``,
+ point is to survive between turns (docker/podman with ``container_persistent``,
daytona, modal, etc.). Non-persistent backends (e.g. Morph) still get torn
down at end-of-turn to prevent leakage. The idle reaper
(``_cleanup_inactive_envs``) handles persistent envs once they exceed
@@ -1198,6 +1228,8 @@ def terminal_tool(
# Select image based on env type, with per-task override support
if env_type == "docker":
image = overrides.get("docker_image") or config["docker_image"]
+ elif env_type == "podman":
+ image = overrides.get("podman_image") or config["podman_image"]
elif env_type == "singularity":
image = overrides.get("singularity_image") or config["singularity_image"]
elif env_type == "modal":
@@ -1268,7 +1300,7 @@ def terminal_tool(
}
container_config = None
- if env_type in ("docker", "singularity", "modal", "daytona"):
+ if env_type in ("docker", "podman", "singularity", "modal", "daytona"):
container_config = {
"container_cpu": config.get("container_cpu", 1),
"container_memory": config.get("container_memory", 5120),
@@ -1279,6 +1311,29 @@ def terminal_tool(
"docker_mount_cwd_to_workspace": config.get("docker_mount_cwd_to_workspace", False),
}
+ if env_type == "podman":
+ podman_user = config.get("podman_user", "")
+ podman_userns = config.get("podman_userns", "")
+ podman_extra_args = config.get("podman_extra_args", [])
+ podman_extra_capabilities = config.get("podman_extra_capabilities", [])
+ podman_privilged = config.get("podman_privileged", False)
+ podman_rootful = config.get("podman_rootful", False)
+
+ if str(podman_user).strip():
+ container_config["podman_user"] = podman_user
+
+ if str(podman_userns).strip():
+ container_config["podman_userns"] = podman_userns
+
+ if isinstance(podman_extra_args, list) and all(podman_extra_args, lambda x: isinstance(x, str)):
+ container_config["podman_extra_args"] = podman_extra_args
+
+ if isinstance(podman_extra_capabilities, list) and all(podman_extra_capabilities, lambda x: isinstance(x, str)):
+ container_config["podman_extra_capabilities"] = podman_extra_capabilities
+
+ container_config["podman_privileged"] = podman_privilged
+ container_config["podman_rootful"] = podman_rootful
+
local_config = None
if env_type == "local":
local_config = {
@@ -1589,6 +1644,15 @@ def check_terminal_requirements() -> bool:
result = subprocess.run([docker, "version"], capture_output=True, timeout=5)
return result.returncode == 0
+ elif env_type == "podman":
+ from tools.environments.podman import find_podman
+ podman = find_podman()
+ if not podman:
+ logger.error("Podman executable not found in PATH or common install locations")
+ return False
+ result = subprocess.run([podman, "version"], capture_output=True, timeout=5)
+ return result.returncode == 0
+
elif env_type == "singularity":
executable = shutil.which("apptainer") or shutil.which("singularity")
if executable:
@@ -1666,7 +1730,7 @@ def check_terminal_requirements() -> bool:
else:
logger.error(
- "Unknown TERMINAL_ENV '%s'. Use one of: local, docker, singularity, "
+ "Unknown TERMINAL_ENV '%s'. Use one of: local, docker, podman, singularity, "
"modal, daytona, ssh.",
env_type,
)
@@ -1685,6 +1749,7 @@ def check_terminal_requirements() -> bool:
print("\nCurrent Configuration:")
print(f" Environment type: {config['env_type']}")
print(f" Docker image: {config['docker_image']}")
+ print(f" Podman image: {config['podman_image']}")
print(f" Modal image: {config['modal_image']}")
print(f" Working directory: {config['cwd']}")
print(f" Default timeout: {config['timeout']}s")
@@ -1707,8 +1772,9 @@ def check_terminal_requirements() -> bool:
print("\nEnvironment Variables:")
default_img = "nikolaik/python-nodejs:python3.11-nodejs20"
- print(f" TERMINAL_ENV: {os.getenv('TERMINAL_ENV', 'local')} (local/docker/singularity/modal/daytona/ssh)")
+ print(f" TERMINAL_ENV: {os.getenv('TERMINAL_ENV', 'local')} (local/docker/podman/singularity/modal/daytona/ssh)")
print(f" TERMINAL_DOCKER_IMAGE: {os.getenv('TERMINAL_DOCKER_IMAGE', default_img)}")
+ print(f" TERMINAL_PODMAN_IMAGE: {os.getenv('TERMINAL_PODMAN_IMAGE', f'docker.io/{default_img}')}")
print(f" TERMINAL_SINGULARITY_IMAGE: {os.getenv('TERMINAL_SINGULARITY_IMAGE', f'docker://{default_img}')}")
print(f" TERMINAL_MODAL_IMAGE: {os.getenv('TERMINAL_MODAL_IMAGE', default_img)}")
print(f" TERMINAL_DAYTONA_IMAGE: {os.getenv('TERMINAL_DAYTONA_IMAGE', default_img)}")
diff --git a/website/docs/developer-guide/architecture.md b/website/docs/developer-guide/architecture.md
index 13f08b7db42ef..ab780ce37f40a 100644
--- a/website/docs/developer-guide/architecture.md
+++ b/website/docs/developer-guide/architecture.md
@@ -106,7 +106,7 @@ hermes-agent/
│ ├── credential_files.py # File-based credential passthrough
│ ├── env_passthrough.py # Env var passthrough for sandboxes
│ ├── ansi_strip.py # ANSI escape stripping
-│ └── environments/ # Terminal backends (local, docker, ssh, modal, daytona, singularity)
+│ └── environments/ # Terminal backends (local, docker, podman, ssh, modal, daytona, singularity)
│
├── gateway/ # Messaging platform gateway
│ ├── run.py # GatewayRunner — message dispatch (~7,500 lines)
@@ -211,7 +211,7 @@ A shared runtime resolver used by CLI, gateway, cron, ACP, and auxiliary calls.
### Tool System
-Central tool registry (`tools/registry.py`) with 47 registered tools across 20 toolsets. Each tool file self-registers at import time. The registry handles schema collection, dispatch, availability checking, and error wrapping. Terminal tools support 6 backends (local, Docker, SSH, Daytona, Modal, Singularity).
+Central tool registry (`tools/registry.py`) with 47 registered tools across 20 toolsets. Each tool file self-registers at import time. The registry handles schema collection, dispatch, availability checking, and error wrapping. Terminal tools support 7 backends (local, Docker, Podman, SSH, Daytona, Modal, Singularity).
→ [Tools Runtime](./tools-runtime.md)
diff --git a/website/docs/developer-guide/creating-skills.md b/website/docs/developer-guide/creating-skills.md
index 7ca16bff5c07a..2b4772051ad0c 100644
--- a/website/docs/developer-guide/creating-skills.md
+++ b/website/docs/developer-guide/creating-skills.md
@@ -248,7 +248,7 @@ Each entry supports:
- `description` (optional) — explains what the file is and how it's created
When loaded, Hermes checks if these files exist. Missing files trigger `setup_needed`. Existing files are automatically:
-- **Mounted into Docker** containers as read-only bind mounts
+- **Mounted into Docker/Podman** containers as read-only bind mounts
- **Synced into Modal** sandboxes (at creation + before each command, so mid-session OAuth works)
- Available on **local** backend without any special handling
diff --git a/website/docs/developer-guide/environments.md b/website/docs/developer-guide/environments.md
index 3409f30473628..0031377771394 100644
--- a/website/docs/developer-guide/environments.md
+++ b/website/docs/developer-guide/environments.md
@@ -84,7 +84,7 @@ The foundation from `atroposlib`. Provides:
### HermesAgentBaseEnv
The hermes-agent layer (`environments/hermes_base_env.py`). Adds:
-- **Terminal backend configuration** — sets `TERMINAL_ENV` for sandboxed execution (local, Docker, Modal, Daytona, SSH, Singularity)
+- **Terminal backend configuration** — sets `TERMINAL_ENV` for sandboxed execution (local, Docker, Podman, Modal, Daytona, SSH, Singularity)
- **Tool resolution** — `_resolve_tools_for_group()` calls hermes-agent's `get_tool_definitions()` to get the right tool schemas based on enabled/disabled toolsets
- **Agent loop integration** — `collect_trajectory()` runs `HermesAgentLoop` and scores the result
- **Two-phase operation** — Phase 1 (OpenAI server) for eval/SFT, Phase 2 (VLLM ManagedServer) for full RL with logprobs
@@ -113,7 +113,7 @@ Your environment inherits from `HermesAgentBaseEnv` and implements five methods:
3. Append tool results to the conversation, go back to step 1
4. If no `tool_calls`, the agent is done
-Tool calls execute in a thread pool (`ThreadPoolExecutor(128)`) so that async backends (Modal, Docker) don't deadlock inside Atropos's event loop.
+Tool calls execute in a thread pool (`ThreadPoolExecutor(128)`) so that async backends (Modal, Docker, Podman) don't deadlock inside Atropos's event loop.
Returns an `AgentResult`:
@@ -426,7 +426,7 @@ See `environments/benchmarks/yc_bench/yc_bench_env.py` for a clean, well-documen
| `max_agent_turns` | `int` | `30` | Max LLM calls per rollout |
| `agent_temperature` | `float` | `1.0` | Sampling temperature |
| `system_prompt` | `str` | `None` | System message for the agent |
-| `terminal_backend` | `str` | `"local"` | `local`, `docker`, `modal`, `daytona`, `ssh`, `singularity` |
+| `terminal_backend` | `str` | `"local"` | `local`, `docker`, `podman`, `modal`, `daytona`, `ssh`, `singularity` |
| `terminal_timeout` | `int` | `120` | Seconds per terminal command |
| `terminal_lifetime` | `int` | `3600` | Max sandbox lifetime |
| `dataset_name` | `str` | `None` | HuggingFace dataset identifier |
diff --git a/website/docs/developer-guide/tools-runtime.md b/website/docs/developer-guide/tools-runtime.md
index 8e349a505d616..52f8cb225bcb4 100644
--- a/website/docs/developer-guide/tools-runtime.md
+++ b/website/docs/developer-guide/tools-runtime.md
@@ -223,6 +223,7 @@ The terminal system supports multiple backends:
- local
- docker
+- podmand
- ssh
- singularity
- modal
diff --git a/website/docs/getting-started/quickstart.md b/website/docs/getting-started/quickstart.md
index bd26f1eebbc94..1471a64a05efb 100644
--- a/website/docs/getting-started/quickstart.md
+++ b/website/docs/getting-started/quickstart.md
@@ -127,10 +127,11 @@ Here are some things to try next:
### Set up a sandboxed terminal
-For safety, run the agent in a Docker container or on a remote server:
+For safety, run the agent in a Docker/Podman container or on a remote server:
```bash
hermes config set terminal.backend docker # Docker isolation
+hermes config set terminal.backend podman # Podman isolation
hermes config set terminal.backend ssh # Remote server
```
diff --git a/website/docs/getting-started/termux.md b/website/docs/getting-started/termux.md
index 1ad71e5313a0e..6cf11a13c580b 100644
--- a/website/docs/getting-started/termux.md
+++ b/website/docs/getting-started/termux.md
@@ -33,7 +33,7 @@ A few features still need desktop/server-style dependencies that are not publish
- `.[all]` is not supported on Android today
- the `voice` extra is blocked by `faster-whisper -> ctranslate2`, and `ctranslate2` does not publish Android wheels
- automatic browser / Playwright bootstrap is skipped in the Termux installer
-- Docker-based terminal isolation is not available inside Termux
+- Docker/Podman-based terminal isolation is not available inside Termux
That does not stop Hermes from working well as a phone-native CLI agent — it just means the recommended mobile install is intentionally narrower than the desktop/server install.
@@ -224,7 +224,7 @@ python -m pip install -e '.[termux]' -c constraints-termux.txt
## Known limitations on phones
-- Docker backend is unavailable
+- Docker/Podman backend is unavailable
- local voice transcription via `faster-whisper` is unavailable in the tested path
- browser automation setup is intentionally skipped by the installer
- some optional extras may work, but only `.[termux]` is currently documented as the tested Android bundle
diff --git a/website/docs/guides/tips.md b/website/docs/guides/tips.md
index 4d21b73579c50..71bb63ec4bf57 100644
--- a/website/docs/guides/tips.md
+++ b/website/docs/guides/tips.md
@@ -175,9 +175,9 @@ On messaging platforms, sessions auto-reset after idle time (default: 24 hours)
## Security
-### Use Docker for Untrusted Code
+### Use Docker/Podman for Untrusted Code
-When working with untrusted repositories or running unfamiliar code, use Docker or Daytona as your terminal backend. Set `TERMINAL_BACKEND=docker` in your `.env`. Destructive commands inside a container can't harm your host system.
+When working with untrusted repositories or running unfamiliar code, use Docker, Podman, or Daytona as your terminal backend. Set `TERMINAL_BACKEND=docker` in your `.env`. Destructive commands inside a container can't harm your host system.
```bash
# In your .env:
diff --git a/website/docs/index.md b/website/docs/index.md
index 0f180673ac41d..7b44889d246b6 100644
--- a/website/docs/index.md
+++ b/website/docs/index.md
@@ -45,7 +45,7 @@ It's not a coding copilot tethered to an IDE or a chatbot wrapper around a singl
## Key Features
- **A closed learning loop** — Agent-curated memory with periodic nudges, autonomous skill creation, skill self-improvement during use, FTS5 cross-session recall with LLM summarization, and [Honcho](https://github.com/plastic-labs/honcho) dialectic user modeling
-- **Runs anywhere, not just your laptop** — 6 terminal backends: local, Docker, SSH, Daytona, Singularity, Modal. Daytona and Modal offer serverless persistence — your environment hibernates when idle, costing nearly nothing
+- **Runs anywhere, not just your laptop** — 7 terminal backends: local, Docker, Podman, SSH, Daytona, Singularity, Modal. Daytona and Modal offer serverless persistence — your environment hibernates when idle, costing nearly nothing
- **Lives where you do** — CLI, Telegram, Discord, Slack, WhatsApp, Signal, Matrix, Mattermost, Email, SMS, DingTalk, Feishu, WeCom, BlueBubbles, Home Assistant — 15+ platforms from one gateway
- **Built by model trainers** — Created by [Nous Research](https://nousresearch.com), the lab behind Hermes, Nomos, and Psyche. Works with [Nous Portal](https://portal.nousresearch.com), [OpenRouter](https://openrouter.ai), OpenAI, or any endpoint
- **Scheduled automations** — Built-in cron with delivery to any platform
diff --git a/website/docs/reference/cli-commands.md b/website/docs/reference/cli-commands.md
index c430d3ba870dd..dfd3d1cb415d2 100644
--- a/website/docs/reference/cli-commands.md
+++ b/website/docs/reference/cli-commands.md
@@ -298,7 +298,7 @@ Outputs a compact, plain-text summary of your entire Hermes setup. Designed to b
| **Environment** | OS, Python version, OpenAI SDK version |
| **Identity** | Active profile name, HERMES_HOME path |
| **Model** | Configured default model and provider |
-| **Terminal** | Backend type (local, docker, ssh, etc.) |
+| **Terminal** | Backend type (local, docker, podman, ssh, etc.) |
| **API keys** | Presence check for all 22 provider/tool API keys |
| **Features** | Enabled toolsets, MCP server count, memory provider |
| **Services** | Gateway status, configured messaging platforms |
diff --git a/website/docs/reference/environment-variables.md b/website/docs/reference/environment-variables.md
index 56511e9139f4c..d7fc26aa9d58f 100644
--- a/website/docs/reference/environment-variables.md
+++ b/website/docs/reference/environment-variables.md
@@ -109,17 +109,24 @@ For native Anthropic auth, Hermes prefers Claude Code's own credential files whe
| Variable | Description |
|----------|-------------|
-| `TERMINAL_ENV` | Backend: `local`, `docker`, `ssh`, `singularity`, `modal`, `daytona` |
+| `TERMINAL_ENV` | Backend: `local`, `docker`, `podman`, `ssh`, `singularity`, `modal`, `daytona` |
| `TERMINAL_DOCKER_IMAGE` | Docker image (default: `nikolaik/python-nodejs:python3.11-nodejs20`) |
| `TERMINAL_DOCKER_FORWARD_ENV` | JSON array of env var names to explicitly forward into Docker terminal sessions. Note: skill-declared `required_environment_variables` are forwarded automatically — you only need this for vars not declared by any skill. |
| `TERMINAL_DOCKER_VOLUMES` | Additional Docker volume mounts (comma-separated `host:container` pairs) |
| `TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE` | Advanced opt-in: mount the launch cwd into Docker `/workspace` (`true`/`false`, default: `false`) |
| `TERMINAL_SINGULARITY_IMAGE` | Singularity image or `.sif` path |
+| `TERMINAL_PODMAN_IMAGE` | Podman container image |
| `TERMINAL_MODAL_IMAGE` | Modal container image |
| `TERMINAL_DAYTONA_IMAGE` | Daytona sandbox image |
| `TERMINAL_TIMEOUT` | Command timeout in seconds |
| `TERMINAL_LIFETIME_SECONDS` | Max lifetime for terminal sessions in seconds |
| `TERMINAL_CWD` | Working directory for all terminal sessions |
+| `TERMINAL_PODMAN_USERNS` | User namespace remapping mode for Podman backend, corresponding to the `--userns` option of `podman run` |
+| `TERMINAL_PODMAN_USER` | User to use inside Podman container, corresponding to the `--user` option of `podman run` |
+| `TERMINAL_PODMAN_PRIVILEGED` | Start Podman with extra privilages, corresponding to the `--privileged` option of `podman run` |
+| `TERMINAL_PODMAN_EXTRA_CAPABILITIES` | Give more capabilities to the Podman container, corresponding to the `--cap-add` option of `podman run` |
+| `TERMIANL_PODMAN_EXTRA_ARGS` | Extra, arbitrary arguments and options for `podman run` |
+| `TERMINAL_PODMAN_ROOTFUL` | Whether to run Podman in rootful mode - i.e. `sudo podman run` instead of just `podman run`; proper configuration of sudoers of the host OS required |
| `SUDO_PASSWORD` | Enable sudo without interactive prompt |
For cloud sandbox backends, persistence is filesystem-oriented. `TERMINAL_LIFETIME_SECONDS` controls when Hermes cleans up an idle terminal session, and later resumes may recreate the sandbox rather than keep the same live processes running.
diff --git a/website/docs/user-guide/configuration.md b/website/docs/user-guide/configuration.md
index a8cb23f99abd6..ec1fb080e7b21 100644
--- a/website/docs/user-guide/configuration.md
+++ b/website/docs/user-guide/configuration.md
@@ -75,17 +75,18 @@ For AI provider setup (OpenRouter, Anthropic, Copilot, custom endpoints, self-ho
## Terminal Backend Configuration
-Hermes supports six terminal backends. Each determines where the agent's shell commands actually execute — your local machine, a Docker container, a remote server via SSH, a Modal cloud sandbox, a Daytona workspace, or a Singularity/Apptainer container.
+Hermes supports seven terminal backends. Each determines where the agent's shell commands actually execute — your local machine, a Docker container, a Podman container, a remote server via SSH, a Modal cloud sandbox, a Daytona workspace, or a Singularity/Apptainer container.
```yaml
terminal:
- backend: local # local | docker | ssh | modal | daytona | singularity
+ backend: local # local | docker | podman | ssh | modal | daytona | singularity
cwd: "." # Working directory ("." = current dir for local, "/root" for containers)
timeout: 180 # Per-command timeout in seconds
env_passthrough: [] # Env var names to forward to sandboxed execution (terminal + execute_code)
singularity_image: "docker://nikolaik/python-nodejs:python3.11-nodejs20" # Container image for Singularity backend
modal_image: "nikolaik/python-nodejs:python3.11-nodejs20" # Container image for Modal backend
daytona_image: "nikolaik/python-nodejs:python3.11-nodejs20" # Container image for Daytona backend
+ podman_image: "docker.io/nikolaik/python-nodejs:python3.11-nodejs20" # Container image for Podman backend
```
For cloud sandboxes such as Modal and Daytona, `container_persistent: true` means Hermes will try to preserve filesystem state across sandbox recreation. It does not promise that the same live sandbox, PID space, or background processes will still be running later.
@@ -96,6 +97,7 @@ For cloud sandboxes such as Modal and Daytona, `container_persistent: true` mean
|---------|-------------------|-----------|----------|
| **local** | Your machine directly | None | Development, personal use |
| **docker** | Docker container | Full (namespaces, cap-drop) | Safe sandboxing, CI/CD |
+| **podman** | Podman container | Full (namespaces, cap-drop, rootless by default) | Safe sandboxing, CI/CD |
| **ssh** | Remote server via SSH | Network boundary | Remote dev, powerful hardware |
| **modal** | Modal cloud sandbox | Full (cloud VM) | Ephemeral cloud compute, evals |
| **daytona** | Daytona workspace | Full (cloud container) | Managed cloud dev environments |
@@ -111,7 +113,7 @@ terminal:
```
:::warning
-The agent has the same filesystem access as your user account. Use `hermes tools` to disable tools you don't want, or switch to Docker for sandboxing.
+The agent has the same filesystem access as your user account. Use `hermes tools` to disable tools you don't want, or switch to Docker/Podman for sandboxing.
:::
### Docker Backend
@@ -148,6 +150,59 @@ terminal:
**Credential forwarding:** Env vars listed in `docker_forward_env` are resolved from your shell environment first, then `~/.hermes/.env`. Skills can also declare `required_environment_variables` which are merged automatically.
+### Podman Backend
+
+Very similar to the Docker backend, except it also runs in rootless mode by default and supports user namespace remapping out of the box.
+
+These traits are very useful for two reason:
+1. Many images set `root` as the default user. This could be dangerous with a default Docker installation, in case the process escapes the container. This is not a problem with a default Podman installation because `root` inside the container is not actually `root` outside the container.
+2. With user namespace remapping, you can mount the SSH and GPG sockets into the container and let the user inside the container use them for passphrase-less SSH and GPG operations - e.g. git commit signing, pushing code to a remote git server over SSH - while keeping encrypted private keys on disk.
+
+```yaml
+terminal:
+ backend: podman
+ # It is recommended that you specify the fully qualified image name, which includes the registry location at the beginning
+ podman_image: "docker.io/nikolaik/python-nodejs:python3.11-nodejs20"
+ docker_mount_cwd_to_workspace: false # Mount launch dir into /workspace
+ docker_forward_env: # Env vars to forward into container
+ - "GITHUB_TOKEN"
+ docker_volumes: # Host directory mounts
+ - "/home/user/projects:/workspace/projects"
+ - "/home/user/data:/data:ro" # :ro for read-only
+
+ # Resource limits
+ container_cpu: 1 # CPU cores (0 = unlimited)
+ container_memory: 5120 # MB (0 = unlimited)
+ container_disk: 51200 # MB (requires overlay2 on XFS+pquota)
+ container_persistent: true # Persist /workspace and /root across sessions
+
+ # Podman-specific options (read the official Podman documentation about what they do)
+ podman_userns: host # User namespace remapping mode (--userns)
+ podman_user: pn # User to use *inside* the container (--user), to override the default specified in the image
+ podman_privileged: false # Enable extra privileges (--privileged)
+ podman_extra_capabilities: [] # List of extra capabilities (--cap-add)
+ podman_extra_args: [] # List of arbitrary extra arguments supported by `docker run`
+ podman_rootful: false # Whether to run Podman in rootful mode - i.e. `sudo podman` instead of just `podman`
+```
+
+**Requirements:** Podman installed and running. Hermes probes `$PATH` plus install locations that are common or mentioned in the official Podman documentation (`/usr/bin/podman`, `/usr/local/bin/podman`, `/opt/homebrew/bin/podman`, `/opt/podman/bin/podman`, `/home/linuxbrew/.linuxbrew/bin/podman`).
+
+If you plan to use Hermes Agent in WSL2, Podman Desktop for Windows *will not* work (unlike the case for Docker Desktop and WSL2), you need to install Podman directly in WSL2.
+
+Given that even the existing Docker backend does not try to cater to Windows, this Podman will also not try to cater to Windows.
+
+**Container lifecycle:** Each session starts a long-lived container (`podman run -d ... sleep 2h`). Commands run via `podman exec` with a login shell. On cleanup, the container is stopped and removed.
+
+**Security hardening:**
+- `--cap-drop ALL` with only `DAC_OVERRIDE`, `CHOWN`, `FOWNER` added back
+- `--security-opt no-new-privileges`
+- `--pids-limit 256`
+- Size-limited tmpfs for `/tmp` (512MB), `/var/tmp` (256MB), `/run` (64MB)
+
+**Credential forwarding:** Env vars listed in `docker_forward_env` are resolved from your shell environment first, then `~/.hermes/.env`. Skills can also declare `required_environment_variables` which are merged automatically.
+
+We reuse some configuration variables introduced by the Docker backend because Docker and Podman are so similar anyway. It makes it slightly less troublesome to switch between Docker and Podman.
+
### SSH Backend
Runs commands on a remote server over SSH. Uses ControlMaster for connection reuse (5-minute idle keepalive). Persistent shell is enabled by default — state (cwd, env vars) survives across commands.
diff --git a/website/docs/user-guide/features/tools.md b/website/docs/user-guide/features/tools.md
index 0adec6f0640ad..d565a6449a7b5 100644
--- a/website/docs/user-guide/features/tools.md
+++ b/website/docs/user-guide/features/tools.md
@@ -56,6 +56,7 @@ The terminal tool can execute commands in different environments:
|---------|-------------|----------|
| `local` | Run on your machine (default) | Development, trusted tasks |
| `docker` | Isolated containers | Security, reproducibility |
+| `podman` | Isolated containers | Security, reproducibility |
| `ssh` | Remote server | Sandboxing, keep agent away from its own code |
| `singularity` | HPC containers | Cluster computing, rootless |
| `modal` | Cloud execution | Serverless, scale |
@@ -66,7 +67,7 @@ The terminal tool can execute commands in different environments:
```yaml
# In ~/.hermes/config.yaml
terminal:
- backend: local # or: docker, ssh, singularity, modal, daytona
+ backend: local # or: docker, podman, ssh, singularity, modal, daytona
cwd: "." # Working directory
timeout: 180 # Command timeout in seconds
```
@@ -119,7 +120,7 @@ Configure CPU, memory, disk, and persistence for all container backends:
```yaml
terminal:
- backend: docker # or singularity, modal, daytona
+ backend: docker # or podman, singularity, modal, daytona
container_cpu: 1 # CPU cores (default: 1)
container_memory: 5120 # Memory in MB (default: 5GB)
container_disk: 51200 # Disk in MB (default: 50GB)
diff --git a/website/docs/user-guide/security.md b/website/docs/user-guide/security.md
index aba476bc107a0..c55d983db4ed0 100644
--- a/website/docs/user-guide/security.md
+++ b/website/docs/user-guide/security.md
@@ -115,7 +115,7 @@ The following patterns trigger approval prompts (defined in `tools/approval.py`)
| `gateway run` with `&`/`disown`/`nohup`/`setsid` | Prevents starting gateway outside service manager |
:::info
-**Container bypass**: When running in `docker`, `singularity`, `modal`, or `daytona` backends, dangerous command checks are **skipped** because the container itself is the security boundary. Destructive commands inside a container can't harm the host.
+**Container bypass**: When running in `docker`, `podman`, `singularity`, `modal`, or `daytona` backends, dangerous command checks are **skipped** because the container itself is the security boundary. Destructive commands inside a container can't harm the host.
:::
### Approval Flow (CLI)