Mero LangChain Tools
Security-conscious file, directory, search, process, and rsync tools for LangChain / LangGraph agents — with scoped permissions, human-in-the-loop approval, and manager/worker delegation, all optional and composable.
Nothing here is forced on you. Every tool works standalone with zero
restrictions if you don't pass a SecurityContext. The security layer is
there for when you do want a sandboxed, auditable agent.
📚 Full documentation: https://surajairi.github.io/langchain-mero-tools/
See the documentation for installation, a complete quickstart, per-tool references, the security/approval model in depth, LangGraph integration, and a guide to writing your own tools on top of the same sandboxing. This README stays a short overview.
Install
# Core package
pip install langchain-mero-tools
# or
uv add langchain-mero-tools
# With LangGraph support
pip install "langchain-mero-tools[langgraph]"
# or
uv add "langchain-mero-tools[langgraph]"
Quick start
from langchain_mero_tools import get_tools
# Fully unrestricted — same as handing the agent File/Directory/Search/
# Process/Rsync tools directly, no sandboxing.
tools = get_tools()
Scoping an agent to a directory
from langchain_mero_tools import SecurityContext, Permission, get_tools
ctx = SecurityContext(
name="worker_1",
allowed_paths=["./workspace"], # can't touch anything outside this
denied_paths=["./workspace/.env"], # denied always wins over allowed
permissions=Permission.READ | Permission.WRITE, # no DELETE, no EXECUTE
)
tools = get_tools(ctx) # file, directory, search, process, rsync — all scoped
Path scoping resolves symlinks/.. before checking, so ../../etc/passwd
style traversal can't escape allowed_paths.
Need several directories with different permissions, or want the agent
addressing files by a short stable name instead of a full host path? Use
paths=[PathEntry(...)] for a virtual mount table — e.g. /reports/q3.csv
instead of /home/user/projects/acme/output/reports/q3.csv, with its own
per-mount allowed_permission/required_permission/denied_permission.
Full details: Security & Sandboxing.
Restricting shell commands
process_tool and rsync_tool consult denied_commands / allowed_commands
on the same SecurityContext. A sensible deny-list (fork bombs, sudo,
rm -rf /, dd, mkfs, sub-shell wrapping, piping a download into a shell,
...) is on by default — override or extend it, or flip to allowlist mode for
a fully locked-down agent:
ctx = SecurityContext(
allowed_paths=["./workspace"],
permissions=Permission.READ | Permission.WRITE | Permission.EXECUTE,
allowed_commands=["git *", "npm run *", "pytest *"], # allowlist: default-deny
)
Pass just denied_commands=[...] (leaving allowed_commands empty) for
default-allow / explicit-deny instead.
Each entry in either list is one of three explicit kinds — the kind is never guessed, on purpose:
| Form | Kind | Matched against |
|---|---|---|
"re:<pattern>" |
regex | anywhere in the command (re.search, case-insensitive) |
contains * ? [ ] |
glob | the whole command (case-insensitive) |
| anything else | plain string | substring, case-insensitive |
This matters: a glob like "git *" is also a syntactically valid,
unanchored regex — "git" followed by zero-or-more spaces — which would match
as a substring anywhere (e.g. inside "legitimate" or "digit_leak") if it
were run through re.search the way earlier versions of this library did.
Globs are always matched against the whole command string instead, so
allowed_commands=["git *"] means "the command has this shape end-to-end,"
not "the command contains this text somewhere." If you want denylist-style
"catch this substring anywhere," use the re: prefix with word boundaries,
e.g. r"re:\bsudo\b".
Treat blocklist-based filtering as best-effort even with these fixes — it's
still string matching, not a real shell parser. allowed_commands (default-
deny) is the safer mode for anything agent-driven with real filesystem
access, and pairing it with process_tool's shell=False mode (below)
closes the class of bypass that blocklists structurally can't catch.
Requiring human approval
Any permission can be routed through an approval backend before the action runs:
from langchain_mero_tools import SecurityContext, Permission, CLIApproval
ctx = SecurityContext(
allowed_paths=["./workspace"],
permissions=Permission.READ | Permission.WRITE | Permission.DELETE,
require_approval_for=Permission.WRITE | Permission.DELETE,
approval=CLIApproval(), # blocking terminal y/N prompt
)
Four backends ship out of the box, all implementing the same
request(ApprovalRequest) -> bool interface:
| Backend | Use case |
|---|---|
CLIApproval() |
local dev, blocking input() prompt |
InterruptApproval() |
LangGraph interrupt() — pauses the graph, resumes via Command(resume=...) from your own UI/API |
CallbackApproval(fn) |
wrap a webhook, Slack bot, DB poll, anything |
AutoApprove() / AutoDeny() |
tests, fully-trusted contexts, fail-safe defaults |
Writing your own is one method:
class MyApproval:
def request(self, req: ApprovalRequest) -> bool:
return my_slack_bot.ask(req.requester, req.action, req.detail)
LangGraph interrupt() example
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START, END
from langgraph.types import Command
from langchain_mero_tools import SecurityContext, Permission, InterruptApproval, make_file_tool
ctx = SecurityContext(
allowed_paths=["./workspace"],
permissions=Permission.WRITE,
require_approval_for=Permission.WRITE,
approval=InterruptApproval(),
)
tool = make_file_tool(ctx)
# ... wire `tool` into a graph node, compile with a checkpointer ...
app = graph.compile(checkpointer=InMemorySaver())
config = {"configurable": {"thread_id": "t1"}}
result = app.invoke({"path": "note.txt", "content": "hi"}, config=config)
# result contains "__interrupt__" — graph is paused, nothing written yet
# Resume from your own UI once a human decides:
app.invoke(Command(resume={"approved": True}), config=config)
Manager/worker delegation
For multi-agent setups, a manager agent can auto-approve worker requests that already fall within the manager's own granted scope — without escalating every single action to a human. Anything the manager itself couldn't do gets deferred (to a human, or wherever you chain next).
from langchain_mero_tools import (
SecurityContext, Permission, PermissionRegistry,
ManagerDelegationApproval, ChainedApproval, CLIApproval,
)
registry = PermissionRegistry()
manager_ctx = SecurityContext(
name="manager_1",
allowed_paths=["./workspace"],
permissions=Permission.READ | Permission.WRITE,
)
registry.register_context("manager_1", manager_ctx)
registry.grant_delegation("manager_1", can_approve_for=["worker_a", "worker_b"])
worker_ctx = SecurityContext(
name="worker_a",
allowed_paths=["./workspace"],
permissions=Permission.READ | Permission.WRITE,
require_approval_for=Permission.WRITE,
approval=ChainedApproval([
ManagerDelegationApproval(registry, manager="manager_1"), # tries manager first
CLIApproval(), # falls back to a human
]),
)
If worker_a requests a WRITE within manager_1's own scope, the manager
decides on the spot. If it exceeds the manager's own permissions (or the
manager wasn't delegated authority over that worker), it falls through to
the next backend in the chain — here, a human via CLI.
Tools reference
All tools are built with make_*_tool(ctx=None) or bundled via
get_tools(ctx, include=[...]).
file_tool—read,write(overwrite/append),edit(find/replace, accepts a list of paths for multi-file edits in one call),copy,move,delete.directory_tool—list(tree view;depth,glob,ignore,include_hidden),create,copy,move,delete(always recursive).file_search_tool—files,content. Backed byripgrepif installed, thengrep, then a pure-Python fallback — works with zero system deps either way.max_resultsis a real global cap regardless of which engine is used: matching subprocess output is streamed and the process is terminated as soon as the cap is hit, rather than letting an external tool scan/emit more than needed on a large tree.process_tool— runs a command; working directory pinned insideallowed_paths; command string checked againstallowed_commands/denied_commands.make_process_tool(ctx, shell=True|False):shell=True(default, backward compatible) runs through a real shell — pipes/&&/redirection all work, but only the outer command string is checked, so chained sub-commands aren't individually validated.shell=Falsesplits the command withshlexand execs it directly with no shell at all:;,|,&&, and backticks are inert (just literal argument text), at the cost of no pipes/chaining/redirection. Recommended for agents with real filesystem access. Output (stdout/stderr) is truncated at 20,000 chars each to avoid blowing up the agent's context.rsync_tool— syncs a source directory to a destination; both paths independently checked against theSecurityContext(so it can't be used to move files outside the sandbox in either direction). Usesrsyncif installed, otherwise a Python copy-based fallback.
Development
pip install -e ".[langgraph]" --group dev
pytest
ruff check src/ tests/
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file langchain_mero_tools-0.0.5.tar.gz.
File metadata
- Download URL: langchain_mero_tools-0.0.5.tar.gz
- Upload date:
- Size: 40.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.29 {"installer":{"name":"uv","version":"0.11.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3b6e260537a59d8e826c82dbe9c83fbdb1c578543111131edc7201f25b09bb22
|
|
| MD5 |
d7cafb2662fb1796365a5e8e0d68f99f
|
|
| BLAKE2b-256 |
f690739a5180bee5254b98961f9d3736f6d009ca6dea2ee4a7c3853e573e9fdd
|
File details
Details for the file langchain_mero_tools-0.0.5-py3-none-any.whl.
File metadata
- Download URL: langchain_mero_tools-0.0.5-py3-none-any.whl
- Upload date:
- Size: 36.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.29 {"installer":{"name":"uv","version":"0.11.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dba9114bd6f10dca237bfacaa01cff36d449519f56e20bad6ed09ea1b731612c
|
|
| MD5 |
968e0d605545d5e97cadbfb626542f70
|
|
| BLAKE2b-256 |
17e76819e4fb43d933cf782a76002f111bd8cedf615f2469a53f5f094a764bb3
|