Skip to content

Security: shyamsn97/recursive-flow

Security

docs/security.md

Security

Trust model

LocalRuntime runs agent Python in your process. Same permissions as your interpreter: filesystem, network, env vars, subprocesses. Use it only for code you'd run yourself.

For untrusted agents, or agents you haven't audited yet, use an isolated runtime:

  • DockerRuntime — a fresh container per session.
  • ModalRuntime — a remote Modal container.
  • E2BRuntime — a remote E2B sandbox.
  • DaytonaRuntime — a remote Daytona sandbox.
  • Custom Runtime — SSH, kubectl exec, Firecracker, gVisor, anything.

Docker isolation knobs

DockerRuntime(
    image="rlmflow:local",
    network="none",           # no outbound traffic
    cpus=1.0,                 # CPU quota
    memory="512m",            # OOM cap
    user="1000:1000",         # non-root
    extra_args=[
        "--read-only",        # read-only rootfs
        "--security-opt", "no-new-privileges",
    ],
    mounts={"./workspace": "/workspace"},
)

Mount only what the agent needs. A hostile agent inside the container can still fill its writable volumes, burn CPU up to the quota, and call any tool you injected.

Engine-level caps

Independent of the runtime:

  • max_depth — recursion limit.
  • max_iters — LLM calls per agent.
  • max_budget — total tokens across the subtree.
  • max_output_length — truncate oversized stdout.
  • max_concurrency — opt into threaded parallel children when set.

Proxied tools

runtime.register_tool(fn) and runtime.register_tools([...]) expose callables to the agent REPL. Tools marked @tool(proxy=True) execute on the host when called from a remote sandbox; local tools are shipped into the sandbox when possible. Working-directory-aware tools run relative to runtime.working_directory.

Tools you register are part of the trust boundary. The container can be sealed off, but any injected tool runs on the host with host privileges. Keep that surface small and validate arguments.

Overrides for approval gates

Override Flow.run_exec(agent, repl, code) to gate, classify, or rewrite code before it touches the runtime. Return the same (suspended, payload) shape as the backend to short-circuit execution with a rejection string:

import rflow

class ReviewingFlow(rflow.Flow):
    def run_exec(self, agent, repl, code: str):
        if "rm -rf" in code and input(f"run? {code}\n> ") != "y":
            return False, "rejected by reviewer"
        return super().run_exec(agent, repl, code)

Wrap the runtime or backend if you want approval at the transport layer. Subclass Runtime.open(...) to return a backend that gates start(...) and resume(...) before delegating to the underlying backend.

There aren't any published security advisories