Skip to main content

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_toolread, write (overwrite/append), edit (find/replace, accepts a list of paths for multi-file edits in one call), copy, move, delete.
  • directory_toollist (tree view; depth, glob, ignore, include_hidden), create, copy, move, delete (always recursive).
  • file_search_toolfiles, content. Backed by ripgrep if installed, then grep, then a pure-Python fallback — works with zero system deps either way. max_results is 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 inside allowed_paths; command string checked against allowed_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=False splits the command with shlex and 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 the SecurityContext (so it can't be used to move files outside the sandbox in either direction). Uses rsync if 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

langchain_mero_tools-0.0.5.tar.gz (40.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

langchain_mero_tools-0.0.5-py3-none-any.whl (36.8 kB view details)

Uploaded Python 3

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

Hashes for langchain_mero_tools-0.0.5.tar.gz
Algorithm Hash digest
SHA256 3b6e260537a59d8e826c82dbe9c83fbdb1c578543111131edc7201f25b09bb22
MD5 d7cafb2662fb1796365a5e8e0d68f99f
BLAKE2b-256 f690739a5180bee5254b98961f9d3736f6d009ca6dea2ee4a7c3853e573e9fdd

See more details on using hashes here.

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

Hashes for langchain_mero_tools-0.0.5-py3-none-any.whl
Algorithm Hash digest
SHA256 dba9114bd6f10dca237bfacaa01cff36d449519f56e20bad6ed09ea1b731612c
MD5 968e0d605545d5e97cadbfb626542f70
BLAKE2b-256 17e76819e4fb43d933cf782a76002f111bd8cedf615f2469a53f5f094a764bb3

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page