Local Tools Loading
Load your own tools.py or –tools file safely — opt-in with PRAISONAI_ALLOW_LOCAL_TOOLS=true
Point PraisonAI at your own tools.py, tools/ folder, or explicit --tools /path/to/file.py — after you flip the opt-in with --allow-local-tools (per-invocation) or PRAISONAI_ALLOW_LOCAL_TOOLS=true (shell-scoped).
from praisonaiagents import Agent
agent = Agent(
name="Notes assistant",
instructions="Answer questions using the user's notes.",
tools=["read_notes"],
)
agent.start("What did I write about caching?")The user enables local tools and asks a question; PraisonAI loads your module and exposes functions to the agent.
Quick Start
- Add a tools.py next to your script
Expose a plain Python function in
tools.py:python # tools.py def read_notes(topic: str) -> str: """Return the user's notes on a topic.""" return open(f"notes/{topic}.md").read()Then use it from an
Agent:python # app.py from praisonaiagents import Agent agent = Agent( name="Notes assistant", instructions="Answer questions using the user's notes.", tools=["read_notes"], ) agent.start("What did I write about caching?")Run with the opt-in env var:
bash PRAISONAI_ALLOW_LOCAL_TOOLS=true python app.py - Use an explicit path from the CLI
Pass an absolute path with
--tools— useful for tools files outside your project directory. Use the--allow-local-toolsflag as a per-invocation alternative to the env var:bash praisonai-code run \ --allow-local-tools \ --tools /absolute/path/to/tools.py \ "Summarise my notes on caching"Explicit user-provided
--toolspaths bypass the CWD boundary check (allow_outside_cwd=True), so absolute paths outside the repo work under the opt-in flag. CWD-derivedtools.py/tools/scanning stays strict against..traversal.
How It Works
The loader checks two conditions before executing your file:
PRAISONAI_ALLOW_LOCAL_TOOLSmust be set totrue(case-insensitive). If not, it logs:- >
Refusing to exec {module_path}: set PRAISONAI_ALLOW_LOCAL_TOOLS=true to enable.
- The resolved path must be inside the current working directory (unless you pass an explicit
--toolsargument). If the path escapes the CWD boundary, it logs: - >
Refusing to exec {path}: outside working directory.
CLI --tools file.py also warns via stdout. When you pass a file path to --tools, --rewrite-tools, --expand-tools, or research --tools, the CLI additionally prints a yellow warning to stdout if the load produced zero functions:
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).
The dual-cause phrasing is deliberate (PR #2935) — the same empty result covers both “env-var unset” and “module has no public functions after functions_only=True, skip_private=True filtering”. When the cause is the unset env var, pass --allow-local-tools on the praisonai run invocation instead of exporting the variable. See Security Environment Variables for the full message table.
Concurrent loads stay isolated
Two agents building at the same time — say, one per tenant in praisonai serve — each get a distinct module object. The loader registers every load under a unique sys.modules key of the form praisonai_userload::{name}::{path}::{uuid}, then drops the entry once your file has finished executing — whether the import succeeded or raised. Nothing else in the process can see or clobber it. If your file raises during import, only that load is discarded; other tenants’ live modules stay intact.
This means concurrent loads of the same tools.py from two Agent builds never share a sys.modules slot, a failed exec cannot pop another live load’s module, and long-lived processes (a TUI that rebuilds its resolver, a praisonai serve worker) don’t accumulate one sys.modules entry per load.
Server import is now side-effect-free
As of PR #4261, import praisonai.api.call no longer runs ./tools.py — even when PRAISONAI_ALLOW_LOCAL_TOOLS=true. Only the server entry point triggers the load:
praisonai call(the CLI verb), orbuild_call_app(load_local_tools=True)when embedding the call server programmatically.
The shared tools registry is reset and rebuilt per load (tools.clear() before each rebuild), so repeated build_call_app(load_local_tools=True) calls in the same process do not stack duplicate tool definitions — they used to append forever, sending the same tool multiple times in every realtime session.update.
See Call → Side-effect-free import + build_call_app() for the call-server embedding details.
Zero-config with .praisonai/tools/
Project-local .praisonai/tools/*.py are auto-loaded on praisonai run without any --tools flag, gated by the same opt-in. Discovery walks user-global ~/.praisonai/tools/ plus the project walk-up to the git root, and each public function is exposed as <module>.<function>.
# Either — per-invocation flag
praisonai run --allow-local-tools "use the greet tool to greet Ada"
# Or — shell-scoped env var
PRAISONAI_ALLOW_LOCAL_TOOLS=true praisonai run "use the greet tool to greet Ada"See Project-Local Tools for the full discovery rules, precedence, and @tool vs plain-callable behaviour.
Discovery Order
When you reference a tool by name (e.g. tools=["read_notes"]), PraisonAI checks four places in this fixed order — first match wins:
- Local
tools.py— backward compatibility, custom tools, custom variables praisonaiagents.tools.TOOL_MAPPINGS— built-in SDK toolspraisonai-toolspackage — external tools (optional install)- Tool registry (plugins via
entry_points) — third-party plugin sources
See Tool Discovery Order for the full breakdown.
LocalManagedAgent uses this same discovery order for its tools=[...] field. Tool names you drop in tools.py are picked up by both Agent(tools=[...]) and LocalManagedAgent(config=LocalManagedConfig(tools=[...])). See Local Managed Agents.
tools.py vs tools/ Folder
When an explicit tools.py is bound via --tools (or found at the CWD root), it takes priority over a sibling tools/ folder. The resolver honours the bound path, so an explicit --tools tools.py no longer silently loses to a sibling tools/ directory. CWD-derived scans still enforce ..-traversal safety.
--tools Outside Your Project
Only explicit user-provided paths — those you pass via --tools /path/to/file.py — opt out of the CWD boundary check. Anything the framework derives (recipe metadata, API-supplied names) stays strict. From the _safe_loader docstring:
“When True, skip the CWD boundary check. Only pass this for paths the user provided explicitly (e.g. a
--toolsCLI argument), never for API/network-derived paths.”
Best Practices
Keep tools.py at the top of your project
Keep tools.py at the top of your project — the CWD boundary check will find it without any flag.
Only set the env var in trusted shells
Only set PRAISONAI_ALLOW_LOCAL_TOOLS=true in trusted shells; a bare pip install will execute the file you point at.
Use absolute paths for tools outside your project
For absolute paths outside your project, pass the full path via --tools — the resolver will honour it under the opt-in flag.
Related
Drop a Python file in .praisonai/tools/ and praisonai run auto-loads it — zero config.
Tool Discovery OrderThe 4-tier pipeline that resolves tool names — local file, built-ins, praisonai-tools, or a plugin.
Approval BackendsChoose who approves a tool call — terminal prompt, plan mode, or a chat channel.