Skip to content

[Feature Request] Go SDK: add Files.ForUser for E2B-compatible filesystem user isolation #1346

Description

@wangxuw

Problem / Motivation

The Go SDK exposes a useful Sandbox.Files() facade, but filesystem calls cannot currently select the sandbox user. This is observable against an AGS-managed E2B-compatible sandbox whose envd requires an explicit user:

failed to stat /tmp: no user specified

The gap affects all current Go filesystem operations:

  • HTTP file operations: Read, Write, and WriteFiles
  • Connect RPC operations: List, Stat/Exists, Remove, Rename, MakeDir, and WatchDir

CommandOptions.User already carries a user for process RPCs, but Files has no equivalent capability. Hard-coding root would fix one environment while preventing correct multi-user isolation.

What E2B currently standardizes

There is no language-neutral E2B type specifically named ReadOptions or WriteOptions. The compatibility contract is the filesystem capability plus its wire behavior, and each official SDK expresses it idiomatically:

  • The official TypeScript SDK defines FilesystemRequestOpts, FilesystemReadOpts, FilesystemWriteOpts, FilesystemListOpts, and WatchOpts. user is a common per-operation request option.
  • The official Python SDK exposes the same capabilities as keyword arguments such as user, request_timeout, gzip, use_octet_stream, metadata, depth, and watch flags.
  • For GET/POST /files, the official SDKs send the identity as the username query parameter.
  • For filesystem Connect RPCs, the official SDKs send Authorization: Basic base64("<user>:"). The filesystem protobuf request messages do not contain a username field.

This means a Go API does not need to copy the TypeScript object shape, but it must preserve these semantics and wire mappings.

Related work: #1126 and #1261 cover Python SDK user isolation. This issue tracks the Go SDK surface and the E2B-compatible HTTP/RPC propagation rules.

Proposed Solution

Add an immutable user-scoped filesystem view:

// ForUser returns a filesystem view that executes every operation as user.
// It does not mutate f; views for different users may be used concurrently.
// An empty user preserves the unscoped behavior of Sandbox.Files().
func (f *Files) ForUser(user string) *Files

Example:

files := sandbox.Files()

rootFiles := files.ForUser("root")
if _, err := rootFiles.Stat(ctx, "/tmp"); err != nil {
	return err
}

nobodyFiles := files.ForUser("nobody")
if _, err := nobodyFiles.MakeDir(ctx, "/tmp/nobody-work"); err != nil {
	return err
}

ForUser should return a shallow, immutable view over the same sandbox transports. It must not mutate the original Files value or store user identity globally on Sandbox.

Transport mapping

Go operation envd transport Scoped behavior
Read GET /files add username=<user> query parameter
Write, WriteFiles POST /files add username=<user> query parameter to every upload
List, Stat, Exists, Remove, Rename, MakeDir, WatchDir filesystem Connect RPC add Authorization: Basic base64("<user>:")

The user must not be inserted into filesystem RPC request bodies. Existing X-Access-Token and traffic-token headers must be retained.

When no user scope is selected, existing SDK behavior should remain unchanged: do not add username or filesystem Basic auth implicitly as part of this feature.

Backward compatibility

  • Keep all existing Files method signatures unchanged in the first change.
  • sandbox.Files().Read(...) and other unscoped calls retain current behavior.
  • ForUser is additive.
  • A user scope is attached to the returned view, not to context.Context and not to global mutable client state.

Future operation-specific options

User identity and operation tuning are different concerns. ForUser should own identity; future options should describe only the operation they configure.

The following is a proposed semantic direction, not a requirement for the first PR:

// ReadOptions controls a buffered file read.
type ReadOptions struct {
	// Gzip asks envd to gzip the response on the wire.
	Gzip bool
}

// FileUploadMode selects how a write is encoded on the wire.
type FileUploadMode uint8

const (
	// FileUploadAuto lets the SDK choose based on data and envd capabilities.
	FileUploadAuto FileUploadMode = iota
	// FileUploadMultipart forces multipart/form-data.
	FileUploadMultipart
	// FileUploadOctetStream requests application/octet-stream.
	FileUploadOctetStream
)

// WriteOptions controls one or more file uploads.
type WriteOptions struct {
	// Gzip compresses the upload and implies octet-stream when supported.
	Gzip bool
	// UploadMode selects automatic, multipart, or octet-stream upload.
	UploadMode FileUploadMode
	// Metadata is persisted as file extended attributes when envd supports it.
	Metadata map[string]string
}

// ListOptions controls directory traversal.
type ListOptions struct {
	// Depth is the maximum listing depth. Zero means the SDK default.
	Depth int
}

// WatchOptions controls filesystem event delivery.
type WatchOptions struct {
	// Recursive watches nested directories.
	Recursive bool
	// IncludeEntry asks envd to attach best-effort entry metadata.
	IncludeEntry bool
	// AllowNetworkMounts permits watches on supported network filesystems.
	AllowNetworkMounts bool
}

Go already has context.Context for cancellation and deadlines, so a generic RequestTimeout or Signal field should not be copied from the TypeScript API without a distinct Go use case. Likewise, User should not be repeated in every options struct.

The E2B TypeScript/Python read(format=...) API can return text, bytes, or a stream dynamically. Go should not vary a method's return type through an option. If needed, use separate typed operations such as Read, ReadBytes, and Open/Reader, sharing semantic read options where appropriate.

Because existing Go interfaces may depend on the current method signatures, the exact Phase 2 exposure (ReadWithOptions, a source-call-compatible variadic form, or a versioned API) should be decided separately. The structs above define the semantic boundaries, not that migration mechanism.

Alternatives Considered

Add one FileOptions to every method

This repeats User across all calls and mixes stable identity scope with unrelated operation controls. It also encourages a large catch-all options type containing fields irrelevant to most methods.

Define separate HTTP and RPC options

Types such as HTTPFileOptions and RPCFileOptions expose an envd implementation detail to SDK callers. A caller should select a user and an operation, not know whether that operation currently uses REST or Connect RPC.

Put the user into filesystem RPC bodies

This does not match the official E2B clients or protobuf contract. E2B-compatible filesystem RPC identity is carried through Basic auth metadata.

Store the user in context.Context

This makes a required execution identity implicit and weakly typed. Context should continue to carry cancellation/deadline/request-scoped metadata, not replace a visible SDK capability.

Always execute filesystem operations as root

This hides the immediate error but breaks permission testing and multi-user isolation, and differs from explicit E2B user selection.

Acceptance Criteria

  • sandbox.Files().ForUser("root").Read/Write/WriteFiles sends username=root on /files requests.
  • sandbox.Files().ForUser("root").List/Stat/Exists/Remove/Rename/MakeDir/WatchDir sends Authorization: Basic cm9vdDo= on filesystem RPCs.
  • Existing access-token and traffic-token headers remain present.
  • ForUser does not mutate the original view; two views for different users can be used concurrently without races or identity leakage.
  • Unscoped calls retain their current request shape.
  • Unit tests assert both HTTP query propagation and RPC header propagation.
  • An integration test demonstrates at least one filesystem operation against an envd deployment that otherwise returns no user specified.

Additional Context

Metadata

Metadata

Labels

enhancementNew feature or requestgoPull requests that update go code

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions