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

Uploaded Python 3

File details

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

File metadata

  • Download URL: langchain_mero_tools-0.0.1.tar.gz
  • Upload date:
  • Size: 40.1 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.1.tar.gz
Algorithm Hash digest
SHA256 03e82a975deb844a1c989c33963680729020dbb9578e4fed7873670c383f2b4a
MD5 7a8b32f7c69993dbfee3f72ded1a08e7
BLAKE2b-256 f49abc5e3c752aef2d5f538fcb0471d5a009fc4efa42ce8ece3e95f82fd95668

See more details on using hashes here.

File details

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

File metadata

  • Download URL: langchain_mero_tools-0.0.1-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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 d6e438fd640f49a31d7236c1a6f94f01349761f9e050c325bfc3be6c81669b28
MD5 0b4bd4687efc209cb840aa053657fade
BLAKE2b-256 883f626fde05746e2785ff01d85bf8984666ed9fa2b013688ae786345ddf47ff

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