workspace

package
v0.0.11 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 10, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Overview

Package workspace defines the JSON messages and security rules of Rainier's workspace-inspection and transfer protocol — the per-repository diff and the bounded push/pull file transfer — together with the path and archive rules both ends of a transfer apply.

It is the single source of truth for these wire-visible types, shared by three programs that speak the protocol: cmd/rainier at one end, cmd/sessiond at the other, internal/controld bridging them. The shapes they exchange have to be ONE definition rather than three that happen to match today.

The security rules are here for a stronger reason still. An archive is untrusted at BOTH ends — a client pushing into somebody's sandbox, and a sandbox answering a pull that lands in a user's home directory — so the checks that keep every entry inside its destination are written once and applied by everyone. A rule implemented twice is a rule that is eventually implemented once.

Index

Constants

View Source
const (
	MethodDiff      = "diff"
	MethodPushFiles = "push_files"
	MethodPullFiles = "pull_files"
)

The three methods controld drives into a sandbox over the session RPC. They are wire words: cmd/sessiond registers handlers under exactly these names (plan §Global Constraints, "RPC methods v0").

View Source
const (
	// MaxBytes caps one transfer's COMPRESSED size, in both directions
	// (design §4.5: 256MiB, refuse over). It is a v0 number chosen to make
	// the one-shot RPC shape safe, not a statement about what a workspace may
	// hold — see the streaming note on the REST handlers.
	MaxBytes int64 = 256 << 20
	// MaxExtractBytes caps what an archive may EXPAND to. The compressed cap
	// says nothing about that: a few hundred megabytes of zeros expand to
	// terabytes, and an extract with no bound of its own would fill the disk
	// of whichever end received it.
	MaxExtractBytes int64 = 4 * MaxBytes
	// ChunkBytes is one chunk's payload. Base64 inflates it by a third, which
	// keeps a chunk under the plan's 2MiB session-RPC payload cap.
	ChunkBytes = 1 << 20
	// SyncEvery is how many chunks sessiond may hold in the page cache before
	// it fsyncs the staging file; the ack for that chunk carries synced:true.
	SyncEvery = 8
	// StatBytes caps one repository's `diff --stat` output.
	StatBytes = 64 << 10
	// WorkspaceRoot is the one directory a transfer may touch inside a
	// session — the mount the session's volume lands on. It is spelled here as
	// well as in cmd/sessiond because controld validates a path BEFORE it
	// reaches a sandbox, and a check that can only run inside the sandbox is a
	// check the sandbox has to be trusted to perform.
	WorkspaceRoot = "/workspace"
)

The transfer's bounds. Every one of them is enforced at more than one hop — the client refuses to send past them, controld refuses to forward past them, and sessiond refuses to write past them — because each hop is the only one that can be sure of the hop before it. In particular a sandbox is not trusted to bound what it sends UP: a pull's total is counted by controld as the bytes arrive.

Variables

View Source
var ErrTooLarge = errors.New("too large")

ErrTooLarge is what every bound above reports. It is a sentinel so a caller can name the cap in its own words — the CLI says which directory was too big, controld says which session, sessiond says which transfer.

Functions

func HumanBytes

func HumanBytes(n int64) string

HumanBytes renders a byte count the way a transfer's messages say it — shared so the cap in an error and the size in a progress line are spelled the same.

func Resolve

func Resolve(root, p string) (string, error)

Resolve turns a transfer path into the absolute path under root it names, refusing anything that would land outside. A relative path is joined to root; an absolute one is honored as given and then checked.

The containment test is on the CLEANED result, which is what makes it a real guard rather than a spelling rule: `a/../../etc` cleans to `../etc` and fails the prefix test even though no single element of it looked wrong.

func TarGz

func TarGz(w io.Writer, dir string, limit int64) (int64, error)

TarGz writes dir as a gzipped tar to w, returning how many COMPRESSED bytes it wrote, and failing with ErrTooLarge the moment that count would pass limit (0 means no limit).

The cap is checked while writing rather than after, because nothing about an archive's size is knowable up front: a directory that compresses to 300MiB has to die mid-stream, having written no more than the cap.

Only directories, regular files and symlinks travel. A socket, a device node or a fifo is refused rather than skipped: silently shipping a tree that is not the tree the user named is the worse failure, and the message says which entry stopped it.

A symlink is held to the SAME rule here as on extraction (checkLink), which is what makes the refusal cheap. Both ends apply it, but only this end can apply it before the bytes move: a tree containing `vendor/node -> /usr/bin/node` would otherwise upload every one of up to 256MiB and fail on the last chunk, in a message about an "archive entry" the user never wrote. Refusing at pack time turns that into an error about a file on their own disk, before the first byte.

func UntarGz

func UntarGz(archive, dest string, limit int64) error

UntarGz extracts the gzipped tar at archive into dest, refusing to write anything at all unless every entry passes first.

TWO PASSES, deliberately. The archive comes from the other end of a transfer, which is never trusted: a tar whose fortieth entry is `../../etc/cron.d/x` would, extracted straight through, have already written thirty-nine files by the time the escape is noticed. Reading the file twice costs one more decompression and buys "an archive is accepted whole or not at all", which is the property the rest of the system relies on. (An I/O failure DURING the second pass — a full disk — can still leave files behind; that is a local accident, not a hostile archive, and the caller's error says so.)

limit bounds the UNCOMPRESSED total; pass MaxExtractBytes unless you have a reason not to.

func ValidatePath

func ValidatePath(p string) error

ValidatePath is the SHAPE check every hop applies to a transfer path before passing it on: the CLI before it sends, controld before it forwards. It rejects an empty path, any `..` element, an embedded NUL, and any absolute path that is not inside WorkspaceRoot.

It is deliberately not the only check. Resolve, below, re-derives containment against the root the file system actually has, and sessiond calls it on every request — because a check performed by the hop before you is a check you are trusting, and the last hop before a syscall trusts nobody.

Types

type DiffAnswer

type DiffAnswer struct {
	Repos []RepoDiff `json:"repos"`
}

DiffAnswer is the whole session's diff, and — passed through unchanged — the body of GET /v0/sessions/{id}/diff. A session with no repositories answers an empty array, never null, which is why MarshalJSON normalizes it.

func (DiffAnswer) MarshalJSON

func (a DiffAnswer) MarshalJSON() ([]byte, error)

MarshalJSON renders a nil Repos as `[]`. The empty answer is the COMMON one (every scratch session), and a client branching on `repos.length` should not have to also branch on null.

type PullChunk

type PullChunk struct {
	Seq  int    `json:"seq"`
	Data []byte `json:"data,omitempty"`
	Done bool   `json:"done,omitempty"`
}

PullChunk is one chunk of a download.

type PullRequest

type PullRequest struct {
	Xfer string `json:"xfer"`
	Path string `json:"path"`
	Seq  int    `json:"seq"`
}

PullRequest asks for one chunk of a download. Seq 0 is what makes the archive: sessiond tars the path on the first request and serves slices of that staging file afterwards, so the bytes a pull returns are one consistent snapshot rather than a directory read live under the reader.

type PushAck

type PushAck struct {
	Seq    int  `json:"seq"`
	Synced bool `json:"synced"`
}

PushAck answers one PushChunk. Synced is true on the chunks sessiond fsynced the staging file for (every SyncEvery chunks, and the last one): it is what a client may believe about durability, and nothing else in this protocol makes that claim.

type PushChunk

type PushChunk struct {
	Xfer string `json:"xfer"`
	Path string `json:"path"`
	Seq  int    `json:"seq"`
	Data []byte `json:"data,omitempty"`
	Done bool   `json:"done,omitempty"`
}

PushChunk is one chunk of an upload, and the body of one POST /v0/sessions/{id}/files. Data is []byte so JSON carries it as base64 with no hand-rolled encoding at any hop.

Xfer names the transfer this chunk belongs to; Path repeats on every chunk so that controld — which holds no per-transfer state, and must not, since a transfer may be answered by any replica — can validate it on every request.

type RepoDiff

type RepoDiff struct {
	Repo          string `json:"repo"`
	BaseBranch    string `json:"base_branch"`
	SessionBranch string `json:"session_branch"`
	Stat          string `json:"stat"`
}

RepoDiff is one repository's answer in a session diff: what it is, the two branches the comparison is between, and git's own `--stat` text.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL