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: see docs/ 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

pip install -e .          # core tools
pip install -e ".[langgraph]"   # + interrupt()-based human approval

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.2.tar.gz (40.3 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.2-py3-none-any.whl (36.7 kB view details)

Uploaded Python 3

File details

Details for the file langchain_mero_tools-0.0.2.tar.gz.

File metadata

  • Download URL: langchain_mero_tools-0.0.2.tar.gz
  • Upload date:
  • Size: 40.3 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.2.tar.gz
Algorithm Hash digest
SHA256 a55d28983dea74c4636604375b5994010412f9ba2b83b76876c9c76c8efccbe6
MD5 4c3511b09dadd82e4d3c8f46a010a8b6
BLAKE2b-256 e75ab57142e395f0f14b712cb06476bdab3b87a7c726f71110dc2d503592cd9d

See more details on using hashes here.

File details

Details for the file langchain_mero_tools-0.0.2-py3-none-any.whl.

File metadata

  • Download URL: langchain_mero_tools-0.0.2-py3-none-any.whl
  • Upload date:
  • Size: 36.7 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.2-py3-none-any.whl
Algorithm Hash digest
SHA256 9612558a83a4d2bfed1e1b71cc277db5784e960e0037d1f792af520715611278
MD5 c3aa2b6f4fa13f2f35d8af886441eb54
BLAKE2b-256 328064e3bfea47b374a7f3d136dad9aa49d65324d52b573607e18f2413a511e3

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