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))))
// done

The 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.