Skip to main content

Filesystem

An inferlet can read and write files inside a single sandboxed directory the host preopens at /scratch. Files persist across inferlet runs as long as the engine is up. This is the right place for episodic memory, checkpoints, and any state that should outlive an individual process. Read this after I/O overview.

The current filesystem story is intentionally narrow: a single guest path (/scratch) backed by one host directory the engine controls. A multi-mount, per-tenant table is on the roadmap; today the contract is "either filesystem access is enabled, or it isn't, and the inferlet sees one directory."

Read and write

The interface is the language's standard filesystem API.

let s: String = std::fs::read_to_string("/scratch/in.txt")
.map_err(|e| e.to_string())?;

std::fs::write("/scratch/out.json", &json_str)
.map_err(|e| e.to_string())?;

Standard filesystem code works as written. The WASI sandbox redirects calls to the preopened directory; paths outside /scratch return permission errors.

Enable filesystem access

Filesystem access is gated by [runtime].allow_fs in ~/.pie/config.toml. It defaults off:

[runtime]
allow_fs = true
# fs_scratch_dir = "/tmp/pie" # optional override; default is <tempdir>/pie

When enabled, the engine creates a host-side scratch directory under [runtime].fs_scratch_dir and preopens a per-process subdirectory at /scratch for every inferlet on this server. Restart the engine to apply.

When disabled (the default), the inferlet's filesystem syscalls return errors immediately. Each inferlet gets its own subdirectory under the configured scratch root.

See Configuration for the full schema.

Patterns

Episodic memory

Every run appends a structured event to a JSONL file. The next run reads recent events back into context.

use std::io::Write;

let mut f = std::fs::OpenOptions::new()
.append(true).create(true)
.open("/scratch/memory.jsonl")
.map_err(|e| e.to_string())?;

writeln!(f, "{}", serde_json::json!({"role": "user", "text": question}).to_string())
.map_err(|e| e.to_string())?;

Checkpoint and resume

Long-running optimization (e.g. ZO) checkpoints adapter weights and progress to disk. The next invocation resumes from the checkpoint.

adapter.save("/scratch/adapters/draft-v1.bin")?;
adapter.load("/scratch/adapters/draft-v1.bin")?;

See Adapters for the full lifecycle.

Per-inferlet subdirectories

Because every inferlet on the engine sees the same /scratch, build a stable per-inferlet directory yourself if you want isolation:

let dir = "/scratch/research-agent";
std::fs::create_dir_all(dir).map_err(|e| e.to_string())?;
std::fs::write(format!("{dir}/state.json"), &json_str)
.map_err(|e| e.to_string())?;

This is a convention, not a sandbox. Other inferlets on the same engine can read and write the same paths.

Sandbox boundary

Inferlets cannot:

  • Access paths outside /scratch. std::fs::read("/etc/passwd") returns Err(NotFound) (or PermissionDenied, depending on the platform).
  • Discover the host's directory structure. The only mount the inferlet sees is /scratch.
  • Talk to other inferlets through the filesystem in any privileged sense — they share the same /scratch, but the runtime does not enforce per-inferlet isolation today.

For the multi-tenant story (per-tenant mounts, read-only paths, host-controlled grants), see the runtime roadmap.

Next

  • HTTP: the network alternative for state that lives elsewhere.
  • MCP: structured tool servers, which often use the filesystem internally.
  • Configuration: the [runtime].allow_fs flag and the rest of the schema.