Skip to content

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 destination if 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 in destination but not in source.

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

rsync_tool.invoke({
    "source": "./workspace/build",
    "destination": "./workspace/deploy",
})

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 copy is simpler if you don't need rsync's incremental/mirror semantics).
  • Security & Sandboxing — why both sides of the sync are checked independently.