Webinar with Seqera and CytoReason: Trace any model back to its data.

Give Your AI agent a Versioned Filesystem: A Self-Correcting Receipts Pipeline on E2B and lakeFS

Alexandria Yip
Last updated on July 8, 2026

Table of Contents

Watch how lakeFS works!

In this post we build something most agent demos skip: an agent that does real work on real data, inside guardrails it can’t escape. The agent turns a messy folder of receipts and invoices into a clean, validated ledger, and it does it on a lakeFS branch mounted as an ordinary filesystem inside an E2B sandbox. The agent writes plain files; underneath, every change is versioned, every run is isolated, and nothing reaches production until it passes a server-side check.

Two pieces make that possible, and they do different jobs:

  • E2B gives the agent a secure place to run: an isolated sandbox — a Firecracker microVM with a Linux kernel — that starts in less than 60 milliseconds, gives the agent a full Linux environment: filesystem access, CLI tools, external API calls, and the ability to run AI-generated code
  • lakeFS gives the agent a safe thing to run on: the control plane for AI-ready data, powered by data version control. Each run gets its own zero-copy branch; intermediate and failed states never touch main; and a pre-merge check decides what gets promoted.

The one-liner we kept coming back to: E2B is where the agent works; lakeFS is what it works on. Let’s build it.


Why this is hard

The moment an agent touches data, you inherit two distinct risks, and most setups only handle one of them.

  • Compute risk. Agent-generated code is unpredictable. It can read the wrong thing, write the wrong thing, or reach the network in ways you didn’t intend. You want it to run somewhere isolated. That rules out more than it sounds like. A generic container runtime gives you process isolation, but not a real kernel underneath. That’s fine if you need to simply execute code, but you need something that behaves like a real machine with Linux, a filesystem, and the ability to mount things.
  • Data risk. Even perfectly behaved code mutates state. Without isolation, every intermediate and failed attempt lands on the same data everyone else depends on, with no record of what changed and no clean way to undo it. You want the data to be versioned and governed.

E2B addresses the first; lakeFS addresses the second. You’d need both before you’d trust an agent near production data.


What we’re building

The full flow, end to end:

  1. A messy inbox/ of receipts lands on a lakeFS repo’s main branch.
  2. The host creates a fresh lakeFS branch for this run.
  3. The host starts an E2B sandbox and mounts that branch at /home/user/mnt.
  4. The agent runs three progressive phases, triage → extract → validate, as plain file I/O on the mount. During validation, the agent may pause and escalate an ambiguous case to a human instead of guessing, then resume once it gets an answer. 
  5. The host commits to lakeFS after each phase, producing an auditable, per-phase history.
  6. A server-side pre-merge gate validates the final ledger; only a clean result merges into main.
  7. Every commit is linked back to the exact E2B sandbox that produced it.

The agent code never imports an S3 client or calls the lakeFS API. It does glob, open, and write. That’s the whole point.

Diagram of E2B sandbox mounting a lakeFS branch via Mount/FUSE
AI Agent versioned filesystem with E2B and lakeFS.

Step 1 — A deliberately messy inbox

We seed main/inbox/ with real-world garbage: receipt photos and scanned invoices across many formats (JPG, PNG, WebP, BMP, TIFF, single- and multi-page PDF), plus exact duplicates, a corrupt file, a non-receipt photo, and an unsupported .txt. This is the “before.” Nothing you’d want to run an agent against unguarded.


Step 2 — Start a sandbox and mount the branch

The host creates the run’s branch in lakeFS, then starts an E2B sandbox and mounts the branch inside it. Credentials reach the sandbox only as environment variables, never on a command line:

from e2b import Sandbox

# Start an isolated E2B sandbox (a Firecracker microVM).
# Using a prebaked template that already contains everest + FUSE + deps.
sbx = Sandbox.create(template="mount-receipts", envs=lakefs_creds, timeout=900)

# Mount the lakeFS branch as a normal filesystem inside the sandbox.
sbx.commands.run(
    f"everest mount lakefs://{repo}/{branch}/ /home/user/mnt "
    "--protocol fuse --write-mode"
)

Two things are worth pausing on, one from each side:

  • It only works because the sandbox is a real microVM. lakeFS Mount (Everest) uses FUSE, which needs a real kernel and /dev/fuse. An E2B sandbox is a Firecracker microVM, so it has both. A thin container generally wouldn’t.
  • The branch is now just a path. From here on, anything that reads or writes /home/user/mnt is reading and writing a versioned lakeFS branch, with zero data-access code.

Step 3 — The agent is just file I/O

Here is the part that makes this pattern click. Inside the sandbox, the agent globs the inbox, reads each image, calls a vision model, and writes structured output. These are ordinary filesystem operations. It has no idea its filesystem is versioned:

import glob, os

# Phase 1 (triage): dedupe by hash, drop corrupt files and non-receipts.
for path in sorted(glob.glob("/home/user/mnt/inbox/*")):
    if is_duplicate(sha256_file(path)):
        continue
    image = load_image_png(path)          # plain open() — None if corrupt
    if image is None:
        continue
    record = extract(image, model="gpt-4o")   # vision model call
    write_json(f"/home/user/mnt/sidecars/{os.path.basename(path)}.json", record)

State flows from one phase to the next through files on the mount, triage.json, ledger_draft.json, ledger.csv, validation/latest_result.json. The same agent code would run unchanged against a local folder. The only difference is that here every write is captured by lakeFS.

Phase 3 is where “execute whatever code the agent generates” gets concrete. Business rules don’t stay fixed, which means the validator function won’t stay fixed. Policy caps change, a new vendor needs its own currency handling, someone adds a rule for invoice-number formats next quarter. Coding and redeploying a validator for every variant doesn’t scale, but letting the agent generate the check from a spec does. Rather than running a validator we shipped, the agent writes the per-receipt validation logic at runtime from a plain-English rule spec and runs that never-before-seen Python inside the sandbox, exactly what an isolated microVM is for. Because that code is model-written and therefore untrusted, the server-side gate in step 6 re-checks its output independently.


Step 4 — Human-In-The-Loop Escalation

Not every case is a clean accept/reject. A low-contrast scan of a receipt may be too illegible to trust. Rather than guess or force a default call, the agent uses its own judgement on whether a case is ambiguous.

# Inside the sandbox: the agent's own generated validator makes the ambiguity call itself. 
Two examples it might use:
if not currency:
    outcome, reason = "ambiguous", "currency could not be determined"
elif 0.01 < abs(total - line_item_sum) <= max(5.00, 0.05 * total):
    outcome, reason = "ambiguous", f"total {total} vs sum(items) {line_item_sum} — possible misread digit"

# On the host, once Phase 3 reports any ambiguous rows — trusted as-is, whatever reason
# the agent gave.
if validation["status"] == "awaiting_review":
    sandbox_id = sbx.beta_pause()
    # sandbox stays paused — no cost, no idle compute — until a human replies

    decision = ask_human(
        channel="#receipts-review",
        message=f"{row['source_file']}: the agent flagged this ambiguous — {row['reason']}. "
                 "Approve, reject, or name the actual currency if that's the issue "
                 "(EUR, GBP, CHF, CAD, or USD).",
    )

    sbx = Sandbox.connect(sandbox_id)   # resumes automatically
    write_decision(sbx, row["source_file"], decision)
    rerun_phase(sbx, "validate")        # the agent applies the decision and commits it

A person answers whenever they get to it on their own time using the pause/resume feature. When the reply comes back, the agent parses it and resumes where it left off, and that decision gets committed to the branch.


Step 5 — Commit after every phase

The host drives the three phases and commits the mount after each one. Because each phase is its own commit, the branch ends up with a readable, auditable history of exactly what the agent did and when:

for i, (phase, name) in enumerate(PHASES, start=1):
    sbx.commands.run(f"python -m mount_receipts.agent_runner {phase} /home/user/mnt")
    sbx.commands.run(f'everest commit /home/user/mnt -m "Phase {i} — {name}"')

Expected output:

Creating branch 'agent-run-20260706-091512' from 'main'...
Sandbox: ivfmkyxxxxxxxxxxxxxxxxv  (template=mount-receipts)
Using prebaked template...
Mounting lakefs://iddo-e2b-receipts-ledger-demo/agent-run-20260706-091512 (FUSE, write-mode)...
─────────────────────────────────────────────────────────
Phase 1 — Triage (structural)
─────────────────────────────────────────────────────────
  {"phase": "triage", "inbox": 15, "kept": 11, "dropped": 4}
  committed: afa3164d7324
─────────────────────────────────────────────────────────
Phase 2 — Extract (multimodal)
─────────────────────────────────────────────────────────
  {"phase": "extract", "kept": 11, "extracted": 11, "extraction_failed": 0}
  committed: 28b779a4ef52
─────────────────────────────────────────────────────────
Phase 3 — Validate (business rules)
─────────────────────────────────────────────────────────
  {"phase": "validate", "status": "awaiting_review", "summary": "2 row(s) flagged ambiguous by the validator — awaiting human review", "pending_review": 2}
  committed: 792aa3f535e2
─────────────────────────────────────────────────────────
2 row(s) flagged ambiguous by the validator — pausing sandbox for human review...
Sandbox paused (ivfmkyxxxxxxxxxxxxxxxxv).
[#receipts-review] receipt_nocurrency.jpg: the agent's validator flagged this ambiguous — currency could not be determined. Approve, reject, or name the actual currency if that's the issue (EUR, GBP, CHF, CAD, or USD).
[#receipts-review] receipt_smudged.jpg: the agent's validator flagged this ambiguous — total 16.25 vs sum(items) 15.75 (off by 0.50) — possible misread digit. Approve, reject, or name the actual currency if that's the issue (EUR, GBP, CHF, CAD, or USD).
... 4 minutes later, a reviewer replies in the thread: "EUR" and "approve" ...
Resuming sandbox with human decisions...
  {"phase": "validate", "status": "passed", "summary": "6 accepted, 5 rejected, 0 dropped — ledger valid"}
  committed: 5c1e9a02d4f1
Run manifest committed (E2B sandbox: https://e2b.dev/dashboard/iddoavneri/sandboxes/ivfmkyxxxxxxxxxxxxxxxxv/monitoring)
Merging 'agent-run-20260706-091512' into 'main'...
Merged. ref: be2f09b7c0e8274592ebb815924bb02f5bf68e430790ec2654f74c452c8804c6
─────────────────────────────────────────────────────────
Final Report
─────────────────────────────────────────────────────────
Branch    : agent-run-20260706-091512
Sandbox   : ivfmkyxxxxxxxxxxxxxxxxv
E2B link  : https://e2b.dev/dashboard/iddoavneri/sandboxes/ivfmkyxxxxxxxxxxxxxxxxv/monitoring
Outcome   : PASSED   merged=True
Ledger    : 6 accepted, 5 rejected, 4 dropped
Reviewed  : 2 row(s) resolved by a human (see validation/human_review_log.json)
Summary   : 6 accepted, 5 rejected, 4 dropped — ledger valid
lakeFS UI: https://myorg.us-east-1.lakefscloud.io/repositories/iddo-e2b-receipts-ledger-demo/objects?ref=agent-run-20260706-091512

lakeFS commit log on the agent-run branch: triage, extract, validate, human review and the manifest commit, then the merge to main.

AI Agent versioned filesystem

Step 6 — A gate the agent cannot talk its way around

Committing every attempt is only safe because not every attempt is allowed to merge. lakeFS runs a server-side pre-merge hook that re-validates the committed ledger independently. It re-derives accept/reject for every row from the data itself (currency, amount vs. line-item sum, the policy cap, dates, and invoice-number uniqueness) instead of trusting the pass/fail the agent reported. It runs on the server, regardless of who or what triggers the merge. Even a buggy or self-serving validator cannot promote bad data:

 # lakefs_actions/validate_ledger.yaml
  name: Ledger Quality Pre-Merge Gate
  on:
    pre-merge:
      branches: [ main ]
  hooks:
    - id: check_ledger_validation
      type: lua
      properties:
        script: |
          local lakefs = require("lakefs")
          local json   = require("encoding/json")
          -- Re-derive the verdict from the committed data, row by row —
          -- don't trust the status the (LLM-written) validator reported.
          local _, body = lakefs.get_object(
            action.repository_id, action.source_ref, "validation/gate_input.json")
          local gate = json.unmarshal(body)
          for _, row in ipairs(gate["rows"]) do
            if row["decided"] == "accepted"
               and (row["currency"] ~= "USD" or row["total"] > gate["policy_cap"]) then
              error("Pre-merge gate FAILED: " .. row["source_file"]
                .. " was accepted but violates policy")
            end
          end
          -- the full hook also re-checks sum-of-line-items, dates, and invoice uniqueness

This is the Write-Audit-Publish pattern, enforced as a Pull Request for data. The agent proposes a change on a branch, the server audits it, and only valid data is published to main.


Step 7 — From any commit back to the sandbox that made it

Reproducibility is built in, not bolted on. When the host commits, it writes the sandbox’s URL into lakeFS commit metadata using lakeFS’s clickable-link convention, so you can jump from a commit straight to the E2B sandbox that produced it:

meta["::lakefs::E2B Sandbox::url[url:ui]"] = (
    f"https://e2b.dev/dashboard/{team}/sandboxes/{sbx.sandbox_id}/monitoring"
)
Agent run manifest - E2B sandbox link

What makes this practical at scale

Here’s why this approach works when you apply it to real-world datasets. These advantages come from the combination, not from either product on its own.

Property
What We Saw
Why It Matters

Fast starts

Baking everest + FUSE + deps into a custom E2B template dropped a ready-to-mount sandbox from ~19s to ~1s (≈3s including the fresh per-run agent-code upload)

You can spin up a fresh, isolated environment per run without paying a setup tax each time

Lazy fetch

A mounted branch of 4.88 GB / 5,000 files fetched only ~9 MB — the files the agent actually opened (just ~6 MB to mount and list all 5,000)

Bring a huge dataset; the agent pays only for the sliver it touches, and the mount is instant regardless of size

Survives pause/resume

After a 30-minute pause + resume, the mount was intact: cached re-reads, fresh reads from the object store, and everest commit all worked

E2B sandboxes pause to save cost; the mount reconnects transparently, with no hang or manual remount

Live filesystem view

The E2B dashboard Filesystem tab shows /home/user/mnt as the agent reads and writes

A real-time window into what the agent is doing to the versioned data

A note on the prebaked template: the template pre-installs Everest, FUSE, and other system dependencies. This ensures the sandbox starts fast, but the agent code is always uploaded fresh so it never runs an outdated version.

We specifically tested pause/resume behavior, since many FUSE/NFS clients drop their connection across a pause and never recover. We tested it directly: mount a branch, read a file, pause the sandbox for 30 minutes (well past typical idle-connection timeouts, so the connection really does have to be re-established), then resume. Afterward the mount was still there. A previously-read file read back from cache, a brand-new file read from the object store, and a write plus everest commit all suceeded. Everest reconnected transparently, no hang, and no manual remount.

E2B Filesystem tab showing the mounted lakeFS branch live at /home/user/mnt during a run

Conclusion

Agents are only as trustworthy as the environment you put them in. E2B gives them a secure, fast, real-Linux place to execute generated code. lakeFS gives them a versioned, branch-isolated, governed view of data they can treat as an ordinary filesystem, with a server-side gate that decides what’s allowed to reach production. The receipts pipeline is a small example, but the shape  (isolated compute on E2B, versioned and gated data on lakeFS, joined by a mount) is exactly what “messy input in, trustworthy output out” agents need.


Try it yourself

The full demo is open and runnable: https://github.com/treeverse/lakeFS-samples/tree/main/01_standalone_examples/e2b/usecases/03-mount-receipts 

You’ll need:

Key links

If you’re building agents that touch real data, we’d love your feedback, especially on the seam where compute isolation and data governance meet.

Frequently Asked Questions

lakeFS Mount (built on the binary) lets you mount a lakeFS branch as an ordinary filesystem inside a sandbox. The agent just does regular file I/O — , , — with no awareness that its filesystem is versioned. Every write it makes is captured by lakeFS underneath. lakeFS Mount is available on lakeFS Enterprise.

A generic container gives you process isolation but shares the host kernel. E2B sandboxes are Firecracker microVMs with a real Linux kernel, which means they support FUSE () — required for lakeFS Mount to work. That’s what makes it safe to let an agent generate and run its own validation code at runtime: even untrusted, never-before-seen Python stays contained inside the microVM.

The agent’s own generated validation logic decides when a case is too uncertain to call automatically. For example, when currency can’t be determined or a total doesn’t match the line-item sum, the host pauses the E2B sandbox (no cost while idle) and escalates to a human reviewer. Once someone replies, the sandbox resumes automatically, the agent applies the decision, and it’s committed to the branch.

Every phase of the pipeline is committed to lakeFS, but nothing reaches automatically. A server-side check independently re-derives the correct accept/reject decision for every row from the underlying data — currency, amount vs. line-item sum, policy caps, invoice-number uniqueness — rather than trusting what the agent reported. This runs on the server regardless of who triggers the merge, so even a flawed or self-serving agent can’t promote bad data. It’s the same pattern as a pull request, applied to data instead of code.

An E2B account (sign up at e2b.dev) and a lakeFS instance with Mount enabled — lakeFS Cloud through lakeFS Enterprise is the fastest path to that. The full demo is open source and runnable from the href=”https://github.com/treeverse/lakeFS-samples/tree/main/01_standalone_examples/e2b/usecases/03-mount-receipts”>lakeFS-samples GitHub repo.

We use cookies to improve your experience and understand how our site is used.

Learn more in our Privacy Policy