Security Environment Variables

Control security-sensitive features with environment variables

Security environment variables control opt-in access to potentially dangerous operations, ensuring secure defaults for RCE and session hijacking prevention.

python
import os
from praisonaiagents import Agent

os.environ["PRAISONAI_ALLOW_LOCAL_TOOLS"] = "true"

agent = Agent(
    name="Tool User",
    instructions="Use tools from tools.py to help the user.",
)
agent.start("Calculate using local tools")

The user opts in with PRAISONAI_ALLOW_LOCAL_TOOLS; without it, dangerous local tools stay blocked by default.

Diagram

Security Model

Set

Unset

🚀 App Start

🔍 Env Var?

✅ Allow Feature

🛡️ Block Feature

💼 Safe Mode

⚠️ Risk Mode

Quick Start

  1. Simple Usage

    Enable local tools.py loading before starting an agent:

    bash
    export PRAISONAI_ALLOW_LOCAL_TOOLS=true
    python
    from praisonaiagents import Agent
    
    agent = Agent(name="Tool User", instructions="Use local tools to help.")
    agent.start("Create a report using tools.py")
  2. With Configuration

    Enable job workflows and remote browser access when required:

    bash
    export PRAISONAI_ALLOW_JOB_WORKFLOWS=true
    export PRAISONAI_BROWSER_ALLOW_REMOTE=true
    bash
    praisonai --workflow job_workflow.yaml
    praisonai browser --host 0.0.0.0 --port 8080

How It Works

Diagram
FeatureSecurityCheckPraisonAIUserFeatureSecurityCheckPraisonAIUseralt[Environment Variable Set][Environment Variable Unset]Request FeatureCheck Env VarAllowed/BlockedExecuteResultSecurity Block
PhaseActionDefault Behavior
StartupCheck environment variablesBlock dangerous features
RequestValidate security permissionsAllow only safe operations
ExecuteRun with appropriate restrictionsFail-safe mode active

Environment Variables

PRAISONAI_TOOL_SAFETY

Controls whether dangerous built-in tools (shell exec, file delete/move/copy, code execution) are gated by the approval system.

Default: unset → default preset active (blocks destructive ops in CI, asks on TTY)

bash
# Disable all tool gating (restore pre-4.6.27 behaviour)
export PRAISONAI_TOOL_SAFETY=off

# Equivalent accepted values: off, full, none, 0, false
PRAISONAI_TOOL_SAFETY=off praisonai code "Clean up logs"
ValueBehaviour
unsetdefault preset — denies destructive ops in CI, asks on TTY
off / full / none / 0 / falseBypass all gating (trust the LLM)
safe / read_onlyBlock all tools in DEFAULT_DANGEROUS_TOOLS
defaultExplicit default (same as unset)

Usage Example:

python
from praisonaiagents import Agent

# Equivalent to PRAISONAI_TOOL_SAFETY=off — tool gating disabled
agent = Agent(
    name="Trusted Coder",
    instructions="Run any tool needed",
    tools=["shell", "write_file", "delete_file"],
    approval="bypass",
)
agent.start("Clean up temp files")

PRAISONAI_ALLOW_LOCAL_TOOLS

Controls automatic loading of tools.py files from the current working directory.

Security Risk: Remote Code Execution (RCE) via malicious tools.py files

bash
# Enable local tools loading
export PRAISONAI_ALLOW_LOCAL_TOOLS=true

# Disable (default - secure)
unset PRAISONAI_ALLOW_LOCAL_TOOLS

Affected Components (verified against PR #1658 head 83b8b14c):

  • praisonai wrapper agent generator (generate_crew_and_kickoff and _run_praisonai), now delegating to ToolResolver.get_local_callables() / ToolResolver.get_local_tool_classes()
  • praisonai.tool_resolver.ToolResolver._load_local_tools (single source of truth; the env-var gate itself is enforced by praisonai._safe_loader.load_user_module)
  • praisonai run YAML workflows (recipe tools.py under _run_yaml_workflow)
  • praisonai research --tools <file.py>
  • praisonai chat --rewrite-tools <file.py> and --expand-tools <file.py>
  • Generic CLI _load_tools(tools_path)
  • HTTP API: praisonai.api.call.import_tools_from_file (raises ValueError if disabled)
  • Path-traversal guard: files outside the current working directory are refused even when PRAISONAI_ALLOW_LOCAL_TOOLS=true
  • praisonaiagents.workflows.workflows.AgentFlow (SDK-level: skips tools.py next to the workflow class when unset)
  • praisonai.cli.features.tools.ToolsCLI.action_add — all three source branches (local file, github:, and bare package name) fail closed without the opt-in (PR #5206)
  • praisonai.templates.tool_override.ToolOverrideLoader.load_from_module — raises PermissionError when unset (was ungated; PR #5206)

PRAISONAI_ALLOW_LOCAL_TOOLS accepts only true (case-insensitive) in the wrapper (praisonai). Values like 1, yes, or on are not truthy for the wrapper (unlike PRAISONAI_ALLOW_TEMPLATE_TOOLS).

Usage Example:

python
from praisonaiagents import Agent

# This will only work if PRAISONAI_ALLOW_LOCAL_TOOLS=true
agent = Agent(
    name="Tool User",
    instructions="Use tools from tools.py to help the user"
)

agent.start("Calculate using local tools")

Error & Warning Messages

WhenWhere it appearsMessage
Env var unset or module loads with zero public functions — CLI file-path loaders (--tools, --rewrite-tools, --expand-tools, research --tools)stdout (rich [yellow])Warning: No tools loaded from <path> (module has no public functions, or local tools loading is disabled — set PRAISONAI_ALLOW_LOCAL_TOOLS=true to enable).
Env var unset, agent generatorlogger.warningRefusing to exec %s: set PRAISONAI_ALLOW_LOCAL_TOOLS=true to enable.
Env var unset, HTTP API (api/call.py)raised exceptionValueError("Local tools loading disabled. Set PRAISONAI_ALLOW_LOCAL_TOOLS=true to enable.")
Path outside CWD, env var setlogger.warningRefusing to exec <path>: outside working directory.
Path outside CWD via HTTP API, env var setraised exceptionLocalToolsDisabled("Refusing to exec <path>: outside working directory.")
Env var unset, SDK AgentFlow with tools.py presentlogger.debugSkipping tools.py load for <class>: set PRAISONAI_ALLOW_LOCAL_TOOLS=true
Env var unset, ToolOverrideLoader.load_from_module(...) called directlyraised exceptionPermissionError: PRAISONAI_ALLOW_LOCAL_TOOLS=true is required to import user modules (refusing to load '<module>').
Env var unset, recipe YAML’s tools_source: <dotted> reaches create_tool_registry_with_overrideslogger.warningskipping tools_source '<source>': PRAISONAI_ALLOW_LOCAL_TOOLS=true is required to import user modules (refusing to load '<source>').
Env var unset, praisonai tools add <bare_package_name>stdout (error status)Refusing to add package tools: set PRAISONAI_ALLOW_LOCAL_TOOLS=true to enable (importing a package runs its code; same opt-in the runtime loader requires).

PRAISONAI_ALLOW_TEMPLATE_TOOLS

Controls implicit tools.py autoload by the template tool-override system, both from the current working directory and from a recipe’s template directory.

Security Risk: Remote Code Execution (RCE) when loading recipes/templates from untrusted sources (e.g. recipes fetched from a remote registry)

bash
# Enable template tools autoload
export PRAISONAI_ALLOW_TEMPLATE_TOOLS=1

# Disable (default - secure)
unset PRAISONAI_ALLOW_TEMPLATE_TOOLS

Default: unset → disabled

Accepted truthy values: 1, true, yes, on (case-insensitive, whitespace-stripped)

Affected Components:

  • praisonai.templates.tool_override.create_tool_registry_with_overrides
  • praisonai.templates.tool_override.resolve_tools

Note: Explicit override_files, override_dirs, and tools_sources continue to work without this opt-in and are the recommended way to load custom tools.

Usage Example:

python
from praisonai.templates.tool_override import create_tool_registry_with_overrides

# This will only load implicit tools.py if PRAISONAI_ALLOW_TEMPLATE_TOOLS=1
registry = create_tool_registry_with_overrides(include_defaults=True)

# Explicit loading works without the env var
registry = create_tool_registry_with_overrides(
    override_files=["./my_tools.py"],  # Always works
    include_defaults=True
)

PRAISONAI_AUTH_CONTENT

Loads the entire credential store from a JSON environment variable, bypassing disk I/O entirely (zero-disk mode). Intended for ephemeral containers/CI where OAuth tokens and API keys must not be persisted.

Security value: secret hygiene — not an RCE gate. It reduces on-disk secret sprawl. The env var itself must be treated as a secret; inject it only from a secrets manager, never commit it, and prefer a CI runtime that scrubs env vars from build logs.

bash
# Inject the whole store in memory; nothing is written to disk
export PRAISONAI_AUTH_CONTENT='{"openai":{"api_key":"sk-...","auth_method":"apikey"}}'

# Disable (default - disk store used)
unset PRAISONAI_AUTH_CONTENT

Default: unset → on-disk store at ~/.praisonai/credentials.json is used (unchanged behaviour).

Value shape: a JSON object mapping provider → credential object (not 1/true). Each credential mirrors ProviderCredential — api_key + auth_method for API keys, plus access_token, refresh_token, expires_at for OAuth.

Affected Components (verified against PR #3775 head 52f33362):

  • praisonai_code.cli.configuration.credentials.CredentialStore — __init__ (env-blob parsing), _read_credentials (returns the in-memory blob), _write_credentials (no-ops disk I/O; updates the in-memory store only), and the is_in_memory property
  • praisonai_code.cli.commands.auth.auth_list / auth_status — label the Source column env (in-memory) when the store is in memory

Error paths: the store fails fast rather than silently re-enabling disk persistence.

ValueBehaviour
Empty / whitespace onlyRaises ValueError
Not valid JSONRaises ValueError with the underlying decode error
JSON scalar or array (not an object)Raises ValueError
A provider value that isn’t a JSON objectRaises ValueError naming the offending provider(s)

Usage Example:

python
import json, os

os.environ["PRAISONAI_AUTH_CONTENT"] = json.dumps({
    "openai": {"api_key": "sk-...", "auth_method": "apikey"},
})

from praisonaiagents import Agent

agent = Agent(name="CI Agent", instructions="Answer briefly")
agent.start("What is 2+2?")
# No ~/.praisonai/credentials.json is created — everything stays in memory.

PRAISONAI_CONFIG_CONTENT

Supplies the entire user-config layer as an inline JSON/YAML blob parsed in memory. When set, PraisonAI never reads ~/.praisonai/config.yaml or a walk-up praisonai.yaml project file — the direct sibling of PRAISONAI_AUTH_CONTENT for a fully zero-disk run in ephemeral containers/CI.

Security value: secret hygiene / stateless-run control — not an RCE gate. It keeps config.yaml (MCP server blocks, permissions arrays, model allowlists, hooks) off disk. Treat the env var itself as a secret: inject it from a secrets manager, never commit it, and only use it in CI runtimes that scrub env vars from build logs.

bash
# Inline JSON — nothing on disk is read for the user-config layer
export PRAISONAI_CONFIG_CONTENT='{"agent":{"model":"gpt-4o-mini"},"mcp":{"srv":{"url":"https://mcp.example.com"}}}'
praisonai run "Summarise today's docs changes"
# No ~/.praisonai/config.yaml or ./praisonai.yaml is read.
bash
# Inline YAML (multiline via $'...')
export PRAISONAI_CONFIG_CONTENT=$'agent:\n  model: gpt-4o-mini\npermissions:\n  deny:\n    - "rm -rf /"\n'
praisonai run "Answer briefly"

Default: unset → global + project file discovery (unchanged behaviour).

Value shape: any valid YAML mapping — JSON works because JSON is a strict subset of YAML. Same schema as an on-disk config.yaml. The value must be a mapping, not a list or scalar.

Interpolation: ${VAR}, {env:VAR}, and {file:...} directives still resolve — the blob composes with the same conventions as an on-disk file.

Precedence: occupies the same slot as discovered global/project files. Inline content wins over PRAISONAI_CONFIG (path). Per-key env vars (PRAISONAI_MODEL, …) and CLI flags still override it; managed policy is still enforced on top.

Validation / error paths (verified against PR #3983 head 382c6fd6):

ValueBehaviour
Invalid JSON/YAMLEmits UserWarning: "Ignoring PRAISONAI_CONFIG_CONTENT: value is not valid JSON/YAML." and falls back to file discovery
Valid non-mapping (list, scalar)Emits UserWarning: "Ignoring PRAISONAI_CONFIG_CONTENT: value must be a mapping." and falls back to file discovery
Empty valid mapping ({} or empty YAML doc)Authoritative — treated as an empty user-config layer; disk files are still ignored

Usage Example (auth + config in one env set):

bash
export PRAISONAI_AUTH_CONTENT='{"openai":{"api_key":"sk-...","auth_method":"apikey"}}'
export PRAISONAI_CONFIG_CONTENT='agent:
  model: gpt-4o-mini'
praisonai run "Run my CI task"
# Nothing is written to ~/.praisonai — both auth and config live in memory only.
python
import json, os
from praisonaiagents import Agent

os.environ["PRAISONAI_CONFIG_CONTENT"] = json.dumps({
    "agent": {"model": "gpt-4o-mini"},
    "permissions": {"deny": ["rm -rf /"]},
})

agent = Agent(name="CI Agent", instructions="Answer briefly")
agent.start("What is 2+2?")
# No config.yaml read; no config.yaml written.

PRAISONAI_CONFIG

Names an explicit config file path to load as the user-config layer, replacing the discovered global + project files. Lower precedence than PRAISONAI_CONFIG_CONTENT (inline wins). Useful when the config is materialised via a mounted secret (Docker/Kubernetes secret volume or Kubernetes ConfigMap) but you don’t want it discoverable via file walk-up.

bash
export PRAISONAI_CONFIG=/run/secrets/praisonai-config.yaml
praisonai run "Answer briefly"
# Discovered ~/.praisonai/config.yaml + project praisonai.yaml are ignored.

Default: unset → global + project file discovery (unchanged behaviour).

Value shape: an absolute or tilde-expanded path to any file the resolver accepts (YAML/JSON). Discovered global + project files are ignored while this path is used.

Behaviour on missing / unreadable file: does not hard-fail. Emits UserWarning: "PRAISONAI_CONFIG points at '<path>', which could not be read as a config file; falling back to file discovery." and falls back to discovery. A non-mapping file body is skipped the same way.

Diagram

highest

inline wins

then path

else discovery

CLI flags
e.g. –model

Per-key env vars
PRAISONAI_MODEL, …

User-config layer

PRAISONAI_CONFIG_CONTENT
in-memory JSON/YAML

PRAISONAI_CONFIG
explicit file path

~/.praisonai/config.yaml
+ project praisonai.yaml walk-up

Managed policy
PRAISONAI_MANAGED_CONFIG_URL / _DIR

Built-in defaults

The provenance report (praisonai config provenance) reports a layer of env-config for keys supplied this way — source is env:PRAISONAI_CONFIG_CONTENT for inline content, or the absolute file path for explicit-path mode.

PRAISONAI_ALLOW_PLUGIN_DISCOVERY

Controls automatic discovery and loading of plugins from .praisonai/plugins/ (project-level) and ~/.praisonai/plugins/ (user-level) directories.

Security Risk: Remote Code Execution (RCE) via malicious third-party plugins discovered on sys.path.

bash
# Enable plugin auto-discovery
export PRAISONAI_ALLOW_PLUGIN_DISCOVERY=true

# Disable (default - secure)
unset PRAISONAI_ALLOW_PLUGIN_DISCOVERY

Default: unset → discovery skipped silently (debug log only)

Accepted truthy values: true, 1, yes (case-insensitive, whitespace-stripped). on is not accepted.

Affected Component: praisonaiagents.plugins.manager.PluginManager.discover_and_load_plugins

Default behavior when unset: discover_and_load_plugins() is a no-op and returns 0. A logger.debug message fires: Plugin auto-discovery disabled; set PRAISONAI_ALLOW_PLUGIN_DISCOVERY=true.

Usage Example:

python
import os
os.environ["PRAISONAI_ALLOW_PLUGIN_DISCOVERY"] = "true"

from praisonaiagents import Agent
from praisonaiagents import PluginManager

manager = PluginManager()
loaded = manager.discover_and_load_plugins()
print(f"Loaded {loaded} plugins")

agent = Agent(
    name="Plugin User",
    instructions="Use discovered plugins to help"
)
agent.start("Run task with plugins")

Workaround when off: Register plugins explicitly without enabling discovery:

python
from praisonaiagents import PluginManager

manager = PluginManager()
manager.register_plugin("my_plugin", my_plugin_module)

PRAISONAI_NO_PLUGINS

Suppresses external-plugin discovery for the current process — the inverse of PRAISONAI_ALLOW_PLUGIN_DISCOVERY. Use for clean, deterministic baselines while debugging or in CI.

Security value: reproduces a known-clean run when triaging whether a hook-injecting plugin is influencing behaviour. Does not change persisted .praisonai/config.yaml state.

bash
# Skip external plugins for every command in this shell
export PRAISONAI_NO_PLUGINS=1

# Restore default behaviour
unset PRAISONAI_NO_PLUGINS

Default: unset → plugins load normally

Accepted truthy values: true, 1, yes (case-insensitive, whitespace-stripped). on is not accepted.

Affected Component: praisonaiagents.plugins.manager.PluginManager.discover_entry_points and .auto_discover_plugins — both short-circuit to 0 when the var is truthy or the constructor was called with disabled=True.

Precedence: PluginManager(disabled=True|False) constructor param > PRAISONAI_NO_PLUGINS env var > default (load plugins).

Usage Example:

python
from praisonaiagents.plugins import PluginManager

# Forced-off regardless of the env var (Python parity for --pure)
manager = PluginManager(disabled=True)
manager.discover_entry_points()   # returns 0; no plugins loaded

PRAISONAI_PROJECT_ROOT

Sets the allowed root directory for output_file writes from Agents, Tasks and Workflow steps. Writes outside this root are silently blocked.

Security Risk: Path-traversal write outside the project tree (e.g. output_file="../../etc/passwd").

bash
# Set a specific project root
export PRAISONAI_PROJECT_ROOT=/home/user/myproject

# Default: current working directory at save time
unset PRAISONAI_PROJECT_ROOT

Default: unset → os.getcwd() at save time

Affected Components:

  • praisonaiagents.agent.memory_mixin.MemoryMixin._save_output_to_file (Agent.output_file) — PR #6172 (before #6172 this path checked only the path, not the file, so a hardlink inside the root sharing data with a file outside it could be overwritten)
  • praisonaiagents.agents.agents.AgentTeam.save_output_to_file (Task.output_file) — PR #5770
  • praisonaiagents.workflows.workflows _run_impl + _apply_step_policies (Workflow step output_file, sync + async) — PR #5770
  • Shared helper: praisonaiagents.utils.output_file.write_output_file — PR #5770

Silent-failure semantics: When the output path resolves outside the project root, the save is skipped and Output file %r is outside project root %r; skipping save is logged at logging.warning. No exception is raised. The Agent branch also returns False and prints ⚠️ Output path outside project root: <path> to stdout; the Task and Workflow branches log the warning only.

Usage Example:

python
import os
os.environ["PRAISONAI_PROJECT_ROOT"] = "/home/user/myproject"

from praisonaiagents import Agent

# This saves successfully — path is within project root
agent = Agent(
    name="Writer",
    instructions="Write a report",
    output_file="report.md"
)
agent.start("Summarise the project")

# This is blocked — path escapes the project root
agent2 = Agent(
    name="Writer",
    instructions="Write a report",
    output_file="../report.md"
)
agent2.start("Summarise the project")
# Prints: ⚠️ Output path outside project root: ../report.md

PRAISONAI_MAX_DOWNLOAD_BYTES

Caps how many bytes download_file writes, so an endless or oversized response cannot fill the disk.

Security Risk: Disk-exhaustion DoS from an unbounded download. This is a resource limit, not an SSRF gate — address checks are handled by the shared URL guard (see ALLOW_LOCAL_CRAWL).

bash
# Tighten to 10 MiB
export PRAISONAI_MAX_DOWNLOAD_BYTES=10485760

# Loosen to 1 GiB
export PRAISONAI_MAX_DOWNLOAD_BYTES=1073741824

# Default - 100 MiB
unset PRAISONAI_MAX_DOWNLOAD_BYTES

Default: unset → 104857600 bytes (100 MiB)

Value shape: integer bytes, read on every download_file call. A non-integer value makes the call fail with {"success": False, "error": "Error downloading <url>: invalid literal for int() ..."}.

Affected Component: praisonaiagents.tools.file_tools.FileTools.download_file

Over-cap behaviour: writing stops and the call returns `{“success”: False, “path”: “”, “size”: 0, “error”: “Download exceeds

bytes”}. The partial .part` temp file is removed and any existing file at the destination path is left untouched.

ALLOW_LOCAL_CRAWL

Bypasses SSRF protection for loopback, private, link-local, multicast, and unspecified IP addresses in web crawl tools, OpenAPIToolset and URL knowledge sources.

Security Risk: Server-Side Request Forgery (SSRF) — agents fetching http://169.254.169.254/... (cloud metadata), http://localhost:6379 (Redis), etc.

bash
# Allow crawling local/private addresses
export ALLOW_LOCAL_CRAWL=true

# Disable (default - secure)
unset ALLOW_LOCAL_CRAWL

Default: unset → loopback/private/link-local/multicast/unspecified IPs blocked; returns {"error": "URL blocked by SSRF policy"} per blocked URL

Accepted truthy values: true only — exact, case-sensitive match. This is stricter than other env vars on this page.

Affected Components:

  • praisonaiagents.tools.web_crawl_tools — _is_safe_crawl_url calls is_safe_http_url(url) with no allowlist, so the crawler goes through the strict path.
  • praisonaiagents.tools.openapi_toolset.OpenAPIOperation.__call__ — each outgoing request URL goes through is_safe_http_url with no allowlist. A refusal is returned to the model as "{name} failed: refusing unsafe URL '{url}'" so the turn continues.
  • praisonaiagents.tools.openapi_toolset.OpenAPIToolset._load — spec_url is checked before fetch; a loopback/private URL raises ValueError.
  • praisonaiagents.knowledge.knowledge.Knowledge._process_url — URL knowledge sources go through is_safe_http_url; a loopback/private URL raises ValueError("Refusing to fetch unsafe URL: ...") before MarkItDown.convert() is called.
  • praisonaiagents.tools.url_safety.is_safe_http_url — accepts an explicit allowlist keyword; the web crawler, OpenAPIToolset and Knowledge use the no-allowlist path. SearXNG passes its own loopback allowlist (see SEARXNG_URL_ALLOWLIST).
  • praisonaiagents.tools.url_safety.is_safe_http_url also rejects any address where not ip.is_global, which covers shared/CGNAT space (100.64.0.0/10, e.g. Alibaba Cloud metadata at 100.100.100.200) that the is_private check missed. Every caller above inherits this (PR #5959).
  • praisonaiagents.tools.file_tools.FileTools.download_file — uses the same guard with allow_local=False (see note below).

OpenAPIToolset passes follow_redirects=False to httpx.Client for both the spec fetch and every operation call, so a 302 to an internal host cannot bypass the guard.

One predicate (is_safe_http_url) serves every caller; the SearXNG path opts into a loopback allowlist, the crawler, OpenAPIToolset and Knowledge pass none.

Diagram

web_crawl / OpenAPIToolset / Knowledge

SearXNG

loopback + allowlisted

loopback, not allowlisted

private / link-local / metadata

public

Agent requests a URL

Which caller?

is_safe_http_url
no allowlist

validate_searxng_url
loopback allowlist

IP class?

Allowed

Blocked

Usage Example:

python
import os

# Without ALLOW_LOCAL_CRAWL — blocked
from praisonaiagents import Agent

agent = Agent(
    name="Crawler",
    instructions="Fetch the given URL"
)
result = agent.start("Crawl http://192.168.1.1/api/status")
# Returns: {"error": "URL blocked by SSRF policy"}

# With ALLOW_LOCAL_CRAWL=true — allowed
os.environ["ALLOW_LOCAL_CRAWL"] = "true"
result = agent.start("Crawl http://192.168.1.1/api/status")
# Fetches and returns content

SEARXNG_URL_ALLOWLIST

Adds extra hostnames the SearXNG search call sites may reach, on top of the built-in loopback set (localhost, 127.0.0.1, ::1). For self-hosting SearXNG on a custom host that resolves to loopback.

Security Risk: Server-Side Request Forgery (SSRF) — a misconfigured allowlist that reached private ranges could expose internal services. This env var is designed so it cannot: entries can only exempt the loopback class.

bash
# Allow a self-hosted SearxNG on a named loopback alias, in addition to localhost
export SEARXNG_URL_ALLOWLIST=searxng.local,my-searxng

# Disable (default) — only the built-in loopback set is allowed
unset SEARXNG_URL_ALLOWLIST

Scope: SearXNG only. The web crawler (web_crawl / _is_safe_crawl_url) does not read this list.

Format: comma-separated hostnames — whitespace is trimmed and each host is lowercased. Empty or unset → only the built-in loopback set applies.

Default: unset → SearXNG reaches only localhost, 127.0.0.1, ::1.

Affected Components:

  • praisonaiagents.tools.url_safety._env_allowlist — reads SEARXNG_URL_ALLOWLIST
  • praisonaiagents.tools.url_safety.validate_searxng_url — merges the env list with the built-in loopback set and passes it as the allowlist to is_safe_http_url
  • praisonaiagents.tools.searxng_tools.searxng_search and praisonaiagents.tools.web_search (SearXNG provider)

Usage Example:

python
import os

# Allow a self-hosted SearxNG reachable via a custom loopback hostname
os.environ["SEARXNG_URL_ALLOWLIST"] = "searxng.local"

from praisonaiagents.tools import searxng_search

# Reaches http://searxng.local:32768/search because the host is allowlisted
results = searxng_search("AI news", searxng_url="http://searxng.local:32768/search")

PRAISONAI_ALLOW_JOB_WORKFLOWS

Controls execution of job and hybrid workflow types that can run shell commands and scripts.

Security Risk: Remote Code Execution (RCE) via malicious YAML workflows

bash
# Enable job workflows
export PRAISONAI_ALLOW_JOB_WORKFLOWS=true

# Disable (default - secure)  
unset PRAISONAI_ALLOW_JOB_WORKFLOWS

Workflow Types Affected:

  • Job workflows: Direct shell, Python, and script execution
  • Hybrid workflows: Combined agent + job execution

Usage Example:

yaml
# job_workflow.yaml
type: job
steps:
  - name: setup
    shell: |
      echo "Setting up environment"
      pip install requirements.txt

  - name: process
    python: |
      import os
      result = os.listdir(".")
      print(f"Files: {result}")
bash
# Only works with PRAISONAI_ALLOW_JOB_WORKFLOWS=true
praisonai --workflow job_workflow.yaml

PRAISONAI_JOBS_AGENT_ROOT

Confines the agent_file paths that callers submit to the Jobs API.

Security Risk: Arbitrary file reads via agent_file="/etc/passwd" or ../ escapes

bash
# Default (secure) - unset means the server's current working directory
unset PRAISONAI_JOBS_AGENT_ROOT

# Point at a dedicated jobs directory, or widen the scope explicitly
export PRAISONAI_JOBS_AGENT_ROOT=/srv/praisonai/agents

Paths resolving outside the root are rejected with 400. See Jobs agent_file Root.

PRAISONAI_BROWSER_ALLOW_REMOTE

Controls browser server binding to non-loopback interfaces (0.0.0.0, remote IPs).

Security Risk: WebSocket session hijacking and unauthorized browser access

bash
# Enable remote browser access
export PRAISONAI_BROWSER_ALLOW_REMOTE=true

# Disable (default - secure, localhost only)
unset PRAISONAI_BROWSER_ALLOW_REMOTE

Default Behavior:

  • Binds to 127.0.0.1 (localhost only)
  • Blocks attempts to bind to 0.0.0.0 or remote interfaces

Usage Example:

python
from praisonai.browser import BrowserServer

# This will only bind to 0.0.0.0 if PRAISONAI_BROWSER_ALLOW_REMOTE=true
# Otherwise falls back to 127.0.0.1
server = BrowserServer(host="0.0.0.0", port=8080)
server.start()

PRAISONAI_RUN_SYNC_TIMEOUT

Default maximum seconds the wrapper’s sync-to-async bridge will wait for a coroutine to complete.

Default: 300 (5 minutes)

bash
# Tighten for latency-sensitive servers
export PRAISONAI_RUN_SYNC_TIMEOUT=30

# Loosen for long-running batch jobs
export PRAISONAI_RUN_SYNC_TIMEOUT=3600

Resolved per call, not at import. The wrapper bridge reads and parses this value on every run_sync call via _default_timeout(). A malformed value (e.g. notanumber) falls back to 300.0 instead of crashing import praisonai, and a late-set value (dotenv loaded after import, per-request reconfig) takes effect on the next call.

Applies to:

  • Every praisonai CLI entry and wrapper-based server (gateway, a2u, mcp_server, scheduler) via praisonai._async_bridge.run_sync.
  • Every ACP/LSP agent-centric tool call in praison "…" and praisonai tui launch via the sibling praisonai_code.cli.features.agent_tools._run_sync bridge (added in PR #3361).

The SDK (praisonaiagents) uses its own separate bridge — see Async Bridge for the full map.

PRAISONAI_ALLOW_LOOP_BLOCKING

Opts back into the pre-#4261 behaviour where praisonai._async_bridge.run_sync_or_offload() offloads a coroutine onto a worker thread and joins it from inside a running event loop.

Default: unset → strict mode. From inside a running loop, run_sync_or_offload() raises RuntimeError instead of blocking.

bash
# Opt back into the legacy offload-and-join behaviour (interim migration only)
export PRAISONAI_ALLOW_LOOP_BLOCKING=true

# Disable (default - strict, never blocks the loop)
unset PRAISONAI_ALLOW_LOOP_BLOCKING

Accepted truthy values: 1, true (case-insensitive).

Affected Component: praisonai._async_bridge.run_sync_or_offload (and every wrapper sync entry point that reaches it inside a loop, e.g. PraisonAIAdapter.run() → praisonai.run(...)).

In strict mode (default), a call from inside a running loop prints this RuntimeError — grep the traceback for it:

text
run_sync_or_offload() would block the running event loop for up to 300s. From an async context, await one of:
  - praisonai.arun(...)
  - adapter.arun(...)
  - praisonai._async_bridge.arun_sync_or_offload(coro)
Set PRAISONAI_ALLOW_LOOP_BLOCKING=true to opt into the legacy blocking behaviour.

See Async Bridge → run_sync_or_offload for the full decision diagram.

PRAISONAI_SHIM_LAZY

Opts out of the eager subtree walk in praisonai.cli._shim._register_submodules, falling back to pure lazy _AliasFinder resolution for the praisonai.bots / praisonai.gateway / praisonai.daemon alias shims.

Default: unset → eager registration (required for module-identity correctness).

bash
# Skip the eager subtree walk (faster shim install)
export PRAISONAI_SHIM_LAZY=1

# Disable (default - eager, module-identity preserved)
unset PRAISONAI_SHIM_LAZY

Accepted truthy values: 1, true (case-insensitive).

When to use (both must hold):

  • Nothing in the process relies on mock.patch against the old dotted path (praisonai.bots., praisonai.gateway., praisonai.daemon.*).
  • Minimal-container startup speed matters more than mock-patch parity — the eager walk pulls the moved subtree (and any optional heavy deps) at shim-install time.

See Wrapper → alias_package shim: lazy vs eager for the developer reference.


.env file discovery

The CLIs load .env from the current working directory only (find_dotenv(usecwd=True), override=False).

Security value: key hygiene. Before the fix, praisonai could load .env from any parent of the install prefix — including a monorepo checkout you forgot about. The CLI now searches from the CWD only, so API keys come from the directory you run praisonai from (or from the shell), never from an ancestor of site-packages.

Fixed by: PR #5829 / issue #5820. See .env File Loading for the full reference.


Common Patterns

Development Mode

bash
# Enable all features for development
export PRAISONAI_ALLOW_LOCAL_TOOLS=true
export PRAISONAI_ALLOW_TEMPLATE_TOOLS=true
export PRAISONAI_ALLOW_JOB_WORKFLOWS=true  
export PRAISONAI_BROWSER_ALLOW_REMOTE=true
export PRAISONAI_RUN_SYNC_TIMEOUT=30
export PRAISONAI_TOOL_SAFETY=off   # disable tool gating in dev

# Add to ~/.bashrc or ~/.zshrc for persistence
echo 'export PRAISONAI_ALLOW_LOCAL_TOOLS=true' >> ~/.bashrc

Production Mode

bash
# Secure defaults - explicitly unset dangerous variables
unset PRAISONAI_ALLOW_LOCAL_TOOLS
unset PRAISONAI_ALLOW_TEMPLATE_TOOLS
unset PRAISONAI_ALLOW_JOB_WORKFLOWS
unset PRAISONAI_BROWSER_ALLOW_REMOTE
unset PRAISONAI_ALLOW_LOOP_BLOCKING   # keep strict: never block the loop
unset PRAISONAI_SHIM_LAZY             # keep eager: preserve module identity
export PRAISONAI_RUN_SYNC_TIMEOUT=300

# Or use systemd service with secure environment
# /etc/systemd/system/praisonai.service
[Service]
Environment="PRAISONAI_ALLOW_LOCAL_TOOLS=false"
Environment="PRAISONAI_ALLOW_TEMPLATE_TOOLS=false"
Environment="PRAISONAI_ALLOW_JOB_WORKFLOWS=false"
Environment="PRAISONAI_BROWSER_ALLOW_REMOTE=false"
Environment="PRAISONAI_RUN_SYNC_TIMEOUT=300"
# Leave PRAISONAI_ALLOW_LOOP_BLOCKING and PRAISONAI_SHIM_LAZY unset (secure defaults)

Docker Deployment

dockerfile
# Secure Docker deployment
FROM python:3.11-slim

# Secure defaults - do not set dangerous env vars
# ENV PRAISONAI_ALLOW_LOCAL_TOOLS=true  # DON'T DO THIS

COPY . /app
WORKDIR /app
RUN pip install praisonai

# Only enable specific features if needed
# ENV PRAISONAI_ALLOW_JOB_WORKFLOWS=true  # Only if required

CMD ["python", "-m", "praisonai"]

Migration Guide

Upgrading from Vulnerable Versions

  1. Identify Usage

    Check if you use any of these features:

    • Local tools.py files
    • Recipes / templates that ship a tools.py and rely on it being implicitly loaded
    • Job or hybrid workflows with shell/script execution
    • Browser server binding to 0.0.0.0
    • HTTP API callers that pass a file_path to praisonai.api.call.import_tools_from_file — these now raise ValueError until you opt in
  2. Add Environment Variables
    bash
    # Only add variables for features you actually use
    export PRAISONAI_ALLOW_LOCAL_TOOLS=true      # If you use tools.py
    export PRAISONAI_ALLOW_TEMPLATE_TOOLS=1      # If you rely on implicit template/CWD tools.py autoload
    export PRAISONAI_ALLOW_JOB_WORKFLOWS=true    # If you use job workflows
    export PRAISONAI_BROWSER_ALLOW_REMOTE=true   # If you bind browser to 0.0.0.0
    export PRAISONAI_RUN_SYNC_TIMEOUT=300        # Adjust timeout as needed
  3. Test Functionality

    Verify your existing workflows still work:

    bash
    # Test local tools
    praisonai "Use local tools to help me"
    
    # Test job workflows  
    praisonai --workflow your_job_workflow.yaml
    
    # Test remote browser
    praisonai browser --host 0.0.0.0
  4. Review Security

    Evaluate if you really need each dangerous feature:

    • Can you avoid local tools.py files?
    • Can you use agent workflows instead of job workflows?
    • Can you use localhost-only browser access?

Best Practices

🔒 Principle of Least Privilege

Only enable environment variables for features you actively use. Each variable increases your attack surface.

bash
# BAD - Enables everything
export PRAISONAI_ALLOW_LOCAL_TOOLS=true
export PRAISONAI_ALLOW_JOB_WORKFLOWS=true
export PRAISONAI_BROWSER_ALLOW_REMOTE=true

# GOOD - Only enable what you need
export PRAISONAI_ALLOW_LOCAL_TOOLS=true  # Only if you use tools.py
🏢 Production Environment Isolation

Never enable dangerous variables in production unless absolutely necessary. Use staging environments for testing.

bash
# Production - secure defaults
unset PRAISONAI_ALLOW_LOCAL_TOOLS
unset PRAISONAI_ALLOW_JOB_WORKFLOWS
unset PRAISONAI_BROWSER_ALLOW_REMOTE

# Development/Staging - enable as needed
export PRAISONAI_ALLOW_LOCAL_TOOLS=true
📁 File System Security

When PRAISONAI_ALLOW_LOCAL_TOOLS=true or PRAISONAI_ALLOW_TEMPLATE_TOOLS=1 is set, ensure your working directory doesn’t contain untrusted tools.py files. This is especially risky for recipes fetched from remote registries.

bash
# Check for tools.py before running
ls -la tools.py 2>/dev/null && echo "WARNING: tools.py found"

# Run from clean directory
mkdir -p /tmp/clean_workspace
cd /tmp/clean_workspace
praisonai "Your task here"
🌐 Network Security

When PRAISONAI_BROWSER_ALLOW_REMOTE=true, use firewalls and authentication to protect browser endpoints.

bash
# Use specific IP instead of 0.0.0.0 when possible
export PRAISONAI_BROWSER_ALLOW_REMOTE=true
praisonai browser --host 192.168.1.100 --port 8080

# Consider using reverse proxy with authentication
# nginx, caddy, or similar with basic auth

Security Advisories

These environment variables address the following security vulnerabilities:

AdvisorySeverityDescriptionEnvironment Variable
GHSA-g985-wjh9-qxxcHighRCE via Automatic tools.py ImportPRAISONAI_ALLOW_LOCAL_TOOLS
GHSA-xcmw-grxf-wjhjHighImplicit RCE via template/CWD tools.py autoloadPRAISONAI_ALLOW_TEMPLATE_TOOLS
GHSA-vc46-vw85-3wvmCriticalRCE via job workflow YAMLPRAISONAI_ALLOW_JOB_WORKFLOWS
GHSA-8x8f-54wf-vv92CriticalWebSocket session hijackingPRAISONAI_BROWSER_ALLOW_REMOTE
pendingHighSSRF via web crawl to loopback/private addressesALLOW_LOCAL_CRAWL
#4189HighSSRF — loopback allowlist re-opened web_crawl to 127.0.0.1SEARXNG_URL_ALLOWLIST
#5943HighSSRF via OpenAPIToolset operation URLs, spec_url fetch, and Knowledge._process_url URL ingestionALLOW_LOCAL_CRAWL
#5970HighSSRF via MCP OAuth discovery (scheme-only check, redirects re-validated after the request) and web_crawl urllib fallback (redirects followed into internal hosts)n/a (fixed in-code; no opt-in required)
#5820Medium.env key leakage from the install tree — bare load_dotenv() loaded any .env above the module location, so API keys could silently come from a source checkoutn/a (fixed in-code; no opt-in required)
#5770MediumPath-traversal via Task and Workflow step output_file (templated workflow path with untrusted {{input}})PRAISONAI_PROJECT_ROOT
#6171 / #6172MediumHardlink escape via Agent.output_file — a hardlink inside the project root sharing data with a file outside it could be overwritten (fixed by routing through write_output_file: O_NOFOLLOW + link-count check + os.replace)PRAISONAI_PROJECT_ROOT
#5957HighSSRF via download_file (trailing-dot hosts, unresolvable hosts, ambiguous numeric IPs) + no byte cap + non-atomic destination write; CGNAT/shared space (100.64.0.0/10) not blocked by is_safe_http_urlPRAISONAI_MAX_DOWNLOAD_BYTES, ALLOW_LOCAL_CRAWL

CVE IDs: Pending assignment by GitHub Security Advisory system

Fixed Versions:

  • praisonai: >=0.0.57
  • praisonaiagents: >=0.0.23

PR #1583 (2026-04-30) extended PRAISONAI_ALLOW_LOCAL_TOOLS enforcement to research/rewrite/expand/recipe tool-loading paths and the HTTP API, and added a CWD-only path constraint as defence-in-depth. No new advisory was filed; the threat model is unchanged from GHSA-g985-wjh9-qxxc.

PR #5206 (fix: fail closed on tool-loader opt-in bypass and security config parse errors) extended PRAISONAI_ALLOW_LOCAL_TOOLS enforcement to the praisonai tools add <package_name> bare-package branch and to ToolOverrideLoader.load_from_module — both previously ran user code without the opt-in. The same PR made ~/.praison/security.yaml fail closed on parse errors. No new advisory was filed; the threat model is unchanged from GHSA-g985-wjh9-qxxc.


Related