Rsync Tool¶
rsync_tool syncs a source directory to a destination directory. Both
paths are checked against the SecurityContext independently, so the
tool can't be used to move files outside the sandbox in either direction —
a scoped agent can't use it to exfiltrate files out, or to pull files in
from somewhere it isn't allowed to read.
from langchain_mero_tools import make_rsync_tool
rsync_tool = make_rsync_tool(ctx) # ctx=None for unrestricted access
Uses the rsync binary if it's installed, otherwise falls back to a
Python copy-based sync (shutil + mtime comparison) — so this works in
minimal containers without any extra system packages.
Input schema¶
{
"source": str,
"destination": str,
"delete": bool, # default False — remove destination files not present in source
"dry_run": bool # default False — preview changes without applying them
}
Permissions¶
| What's being checked | Permission required |
|---|---|
source |
READ |
destination |
WRITE, plus DELETE as well if delete=True |
How syncing works¶
With rsync installed¶
Runs rsync -a [--delete] [--dry-run] -v <source>/ <destination>/ and
returns its combined stdout/stderr. Standard rsync semantics apply —
archive mode (-a) preserves permissions, timestamps, symlinks, etc.
Without rsync (pure-Python fallback)¶
Walks both source and destination, and for every file in source:
- Copies it to
destinationif it doesn't exist there yet, or if the source file's modification time is newer than the destination's. - If
delete=True, also removes any file present indestinationbut not insource.
This is additive-by-default and mtime-based, similar in spirit to rsync's default behavior but implemented without the binary. It's not a byte-for- byte reimplementation of rsync's delta-transfer algorithm — it always copies the whole file when a copy is needed, rather than transferring only the changed blocks.
Examples¶
Basic sync¶
Mirror exactly (remove anything in destination not in source)¶
rsync_tool.invoke({
"source": "./workspace/build",
"destination": "./workspace/deploy",
"delete": True,
})
Note this requires Permission.DELETE on the context, since it can remove
files at the destination.
Preview without applying¶
rsync_tool.invoke({
"source": "./workspace/build",
"destination": "./workspace/deploy",
"delete": True,
"dry_run": True,
})
With the Python fallback, a dry run prefixes the action list with [dry
run] and performs no filesystem changes. With the rsync binary, -n /
--dry-run is passed through to rsync itself.
Error responses¶
rsync_tool.invoke({"source": "./missing", "destination": "./out"})
# "Error: source './missing' does not exist."
rsync_tool.invoke({"source": "/etc", "destination": "./workspace/out"})
# "Denied: Path '/etc' is an absolute path that isn't covered by any
# configured PathEntry, and its leading segment doesn't match a mounted slug (...)."
See also¶
- Directory Tool — for a one-shot recursive copy
instead of an ongoing sync (directory
copyis simpler if you don't need rsync's incremental/mirror semantics). - Security & Sandboxing — why both sides of the sync are checked independently.