Confine a workspace
Use caller.withRoot(path) to give a consumer filesystem access within one directory. Bind that caller with
MemoryFileSystem.bindCaller when the consumer requests Effect's FileSystem service.
Create and bind the caller
Install @effect-vfs/core, @effect-vfs/memory, and the matching platform Crypto provider as described in
Getting started. This example creates a workspace, writes through the confined adapter,
and reads the result through the administrator:
import * as BunCrypto from "@effect/platform-bun/BunCrypto"
import { VirtualFileSystem as Vfs } from "@effect-vfs/core"
import { MemoryFileSystem } from "@effect-vfs/memory"
import { Effect, FileSystem } from "effect"
const writeOutput = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
yield* fs.writeFileString("/output.txt", "done")
})
const program = Effect.gen(function* () {
const volume = yield* Vfs.make()
const admin = yield* volume.caller()
yield* admin.mkdir("/workspaces")
yield* admin.mkdir("/workspaces/run-42")
yield* admin.mkdir("/workspaces/run-42/tmp")
const caller = yield* admin.withRoot("/workspaces/run-42")
const fs = yield* MemoryFileSystem.bindCaller(caller)
yield* writeOutput.pipe(Effect.provideService(FileSystem.FileSystem, fs))
return new TextDecoder().decode(yield* admin.readFile("/workspaces/run-42/output.txt"))
}).pipe(Effect.scoped)
console.log(await Effect.runPromise(program.pipe(Effect.provide(BunCrypto.layer))))
// doneThe confined caller's /output.txt resolves to /workspaces/run-42/output.txt. Its absolute symlinks resolve
from the same root, and .. stops there. withDirectory changes the working directory while retaining confinement.
The adapter creates no directories. Include /tmp inside the root when the program uses default temporary paths.
Keep the scope and authority explicit
Keep the derived caller's scope open while the consumer uses it. bindCaller borrows the caller and retains
its credentials and umask. Privileged credentials do not bypass confinement.
Pass only the confined caller or adapter to the consumer. An unrestricted volume, caller, or handle retains its authority. Confinement applies to virtual filesystem names; it does not restrict JavaScript or host APIs. Existing hard links can share file contents across roots.
Account for moves, deletion, and watches
The root keeps its directory identity through rename. Deleting the root permanently invalidates the caller, even if another directory later occupies the same path. Handles lose access when their object has no name inside the root and can regain access when membership returns.
Caller watches filter events to the root and rebase their paths before queueing them. Their default alias: "all"
reports authorized hard-link aliases. The FileSystem adapter selects one path alias and translates caller
failures into PlatformError values. See Compatibility and limits for
handle, temporary cleanup, and watch contracts.