Documentation
¶
Overview ¶
Package agentworkspace is where an agent's tools act: a file tree, processes, a working directory and an environment. A session runs over one Workspace, whichever machine it is on, and every tool that touches files or runs a command goes through it, so a tool written for one runs unchanged on another. Local is the working directory on this machine; a container or a remote runtime is another implementation of the same interface.
The interface is the one the openhands-workspace study settled on: a small value with a file system, one-shot execution and a descriptor the session records. A workspace that can run a process for longer than one call, with pipes to it, is also a Starter.
The package is pre-1.0: its API may change in a minor version.
Index ¶
- Constants
- Variables
- func Forward(ctx context.Context, ws Workspace, port int) (net.Listener, error)
- func Read(w Workspace, name string, max int64) ([]byte, error)
- func Resize(p Process, size TerminalSize) error
- func ValidServerName(name string) bool
- type Command
- type Connector
- type Descriptor
- type Local
- func (l *Local) Close() error
- func (l *Local) Connect(ctx context.Context, port int) (PortConn, error)
- func (l *Local) Descriptor() Descriptor
- func (l *Local) Env() []string
- func (l *Local) Exec(ctx context.Context, c Command) (*Output, error)
- func (l *Local) FS() fs.FS
- func (l *Local) Reach() Reach
- func (l *Local) Real() string
- func (l *Local) Remove(_ context.Context, name string) error
- func (l *Local) Root() string
- func (l *Local) Start(ctx context.Context, c Command) (Process, error)
- func (l *Local) WriteFile(_ context.Context, name string, data []byte, perm fs.FileMode) error
- type LocalOption
- type OfferOption
- type OfferedServer
- type Offering
- type Output
- type OutputFilter
- type PortConn
- type Process
- type Reach
- type Resizer
- type Server
- type ServerProvider
- type Starter
- type TerminalSize
- type Workspace
Constants ¶
const ( KindLocal = "local" KindContainer = "container" KindRemote = "remote" )
Kinds a Descriptor names, as agentsession's env entry records them.
Variables ¶
var ErrNoServer = errors.New("no such server in this workspace")
ErrNoServer is a server name a ServerProvider does not offer.
var ErrOutputFilter = errors.New("the server's output filter failed")
ErrOutputFilter is an OutputFilter that failed: one that gave no reader, or panicked in its call or in a read. The error that wraps it holds the panic's value, which may hold what the filter was given, so a backend that carries a server's start or output across a wire sends this error's words alone, never the wrapping error's.
var ErrOutside = errors.New("path is outside the workspace")
ErrOutside is what a name that leaves the workspace gets, whether it names another directory outright, climbs out with "..", or goes out through a symbolic link.
Functions ¶
func Forward ¶ added in v0.0.11
Forward listens on this machine's loopback alone (127.0.0.1, on a port the system picks) and, for each connection it accepts, opens one stream to port in ws (Connect) and copies both ways until either side ends. It returns the listener, whose Addr is where to connect; closing it, or the end of ctx, stops it and ends every stream it opened. It is a binding helper over Connect: a workspace that is no Connector is errors.ErrUnsupported.
func Read ¶
Read is fs.ReadFile on w's files, at most max bytes of a regular file: a FIFO, a device or a directory is an error, and so is a file over max.
func Resize ¶ added in v0.0.11
func Resize(p Process, size TerminalSize) error
Resize changes the size of p's terminal, and is errors.ErrUnsupported for a process that was started without one.
func ValidServerName ¶ added in v0.0.3
ValidServerName reports whether name may name a Server: one to 32 lowercase letters, digits and inner hyphens, beginning and ending with a letter or a digit. It has no underscore, so a program can prefix a server's tools with its name and a separator of underscores (mcp__<name>__<tool>) without two servers' tools running together.
Types ¶
type Command ¶
type Command struct {
// Args is argv. Args[0] with no slash is looked up as execvp(3)
// does, on the PATH of the environment the process starts with
// (Workspace.Env, then Env), as the workspace's own processes see
// the file system, never on this process's: a relative PATH entry,
// the empty one included, is skipped, and with no PATH the name is
// not found. One with a slash is used as it is, relative to Dir.
// The process gets Args[0] as given.
Args []string
// Dir is relative to Root; "" is Root. A Dir that leaves the
// workspace, by name or through a link, is ErrOutside.
Dir string
// Env is over Workspace.Env, name by name: an entry replaces the
// workspace's of the same name, and a later entry an earlier one,
// so each name reaches the process once.
Env []string
// Stdin is the process's standard input; nil is none. Start refuses
// it: a started process's input is Process.Stdin.
Stdin []byte
// Timeout ends the process, and everything it started, after this
// long; zero is no limit but the context's.
Timeout time.Duration
// Stream, when set, receives the output the caller is not otherwise
// given, as it arrives. For Exec that is standard output and
// standard error, interleaved, and Output holds neither. For Start
// it is standard error, standard output being Process.Stdout; nil
// discards it.
Stream io.Writer
// Terminal, when set, runs the command, Start's alone, with a
// pseudo-terminal of this size as its controlling terminal, in a
// session of its own: Process.Stdin is the terminal's input and
// Process.Stdout its output, standard error merged into it, and
// what is written is echoed as the terminal echoes it. Closing
// Stdin sends the terminal's end of file (^D). Resize changes the
// size. A Stream is refused beside it, since standard error is in
// the output, and Exec refuses it, since it gives the two apart. A
// backend that cannot give a terminal refuses the start with
// errors.ErrUnsupported; it never runs the command on pipes instead.
Terminal *TerminalSize
}
Command is one process to run.
type Connector ¶ added in v0.0.11
type Connector interface {
Workspace
// Connect opens one stream to port on the workspace's loopback.
// ctx bounds the connecting alone; the stream lives until its
// Close, or the workspace's.
Connect(ctx context.Context, port int) (PortConn, error)
}
Connector is a Workspace that can open a byte stream to a TCP port on its loopback, as its own processes reach it: a server a command started there, reached from the program's side. Connecting is as much power as running a command, and a wire grants it with exec.
type Descriptor ¶
type Descriptor struct {
// Kind is KindLocal, KindContainer or KindRemote.
Kind string
// Ref is what the workspace was made from or is reached by: a
// container's image digest, a remote's endpoint. It is empty for
// KindLocal. The machine and the instance are Host and Instance,
// never Ref.
Ref string
// Host names the machine the file system is on, as the program
// names it: a KindLocal workspace sets it to a name that tells this
// machine from the others a session may be resumed on, and a
// container one when its runtime is not on the agent's machine. A
// host name lands in session files a program may publish.
Host string
// Instance names the file system among those Kind, Ref and Host
// describe: a container's or a sandbox's ID, set whenever a restart
// gives a new file system.
Instance string
// Root is Workspace.Root: absolute, or "" when it is withheld.
Root string
}
Descriptor is what the session records about a workspace, in its env entry: agentsession's workspace members (kind, ref, host and instance, every fact that tells this file system from another the session could be resumed against) and the root, which is the entry's cwd. Each is a string a reader compares exactly; one that is empty is not written.
type Local ¶
type Local struct {
// contains filtered or unexported fields
}
Local is a directory on this machine. Every file operation goes through an os.Root, which the kernel-facing layer of the standard library keeps from following a name out of the directory, so a link inside it that points outside is refused rather than followed. A process it runs is not confined: a shell reaches whatever the user does, and the policy is what stands in front of it.
Local runs on Unix: its processes run in process groups of their own, and its files are opened without blocking. Ending a process ends its group and, on Linux, the descendants that left it (Reach).
func NewLocal ¶
func NewLocal(dir string, env []string, opts ...LocalOption) (*Local, error)
NewLocal opens dir as a workspace whose processes start with env and nothing else: nil is an empty environment, not this process's. A product passes this process's environment without its credentials.
func (*Local) Close ¶
Close ends every process Start started that is not closed yet, as Process.Close does, and releases the directory handle.
func (*Local) Connect ¶ added in v0.0.11
Connect opens a stream to port on this machine's loopback, which is the workspace's: 127.0.0.1, and ::1 when nothing listens there. The workspace's Close ends it.
func (*Local) Descriptor ¶
func (l *Local) Descriptor() Descriptor
Descriptor names the local machine: KindLocal, no Ref, Host the system's host name as NewLocal found it (or WithHost's, or hostOr's fallback when the system gives none), and the root.
func (*Local) Env ¶
Env is the environment its processes start with, a copy: a caller that changes it changes nothing a later process starts with.
func (*Local) Exec ¶
Exec runs a command in its own process group. When ctx ends, or Command.Timeout, before the command exits, Exec ends it and what it started with Local's ladder (SIGINT, then SIGTERM, then SIGKILL, a grace apart; see Process) and returns the output so far, with ExitCode -1 and Interrupted or TimedOut; one that ends after the command exited leaves its status as it was. It waits at most two seconds past the command's exit for output its children still hold open.
func (*Local) Reach ¶ added in v0.0.11
Reach is ReachDescendants on Linux while /proc is this process's, where Local walks it for the descendants that left a process's group before it signals, and ReachGroup otherwise (no /proc, an empty one, another PID namespace's, or macOS), where it signals the group alone. It is asked of /proc at each call. On either, a descendant whose parent exited before the end began is not found.
func (*Local) Real ¶
Real is the directory with its links resolved, in which an absolute path the model wrote may be spelled.
func (*Local) Start ¶
Start runs a command in its own process group, with pipes for its standard input and output, and returns at once. Its standard error goes to Command.Stream, or nowhere. The process, and everything it started, is ended by Process.Close, by the end of ctx, by Command.Timeout, or by the workspace's Close, each with Local's ladder (SIGINT, SIGTERM, SIGKILL, a grace apart), which Close begins by closing the process's input; when it exits by itself, anything left in its group is killed.
With Command.Terminal, on Linux, the process runs in a session of its own on a pseudo-terminal; elsewhere the start is errors.ErrUnsupported.
type LocalOption ¶ added in v0.0.11
type LocalOption func(*localConfig)
A LocalOption changes how NewLocal opens a workspace.
func WithHost ¶ added in v0.0.11
func WithHost(name string) LocalOption
WithHost names this machine in the Descriptor, in place of the system's host name, which two machines a session may move between can share. NewLocal refuses an empty name.
type OfferOption ¶ added in v0.0.4
type OfferOption func(*offerConfig)
An OfferOption changes how an Offering starts its servers. The options are this package's own, such as WithOutputFilter.
func WithOutputFilter ¶ added in v0.0.4
func WithOutputFilter(f OutputFilter) OfferOption
WithOutputFilter has every server that names Secrets started with its standard output and standard error read through f. A server that names none is started as without it. It covers what the server writes, and nothing else the workspace runs: see Offer.
type OfferedServer ¶ added in v0.0.3
type OfferedServer struct {
Server
Command Command
// Secrets name the variables of Command.Env whose values are
// secrets, such as the server's token: an Offering with an
// OutputFilter reads the server's output through it, told these
// values. A client is never told of them.
Secrets []string
}
OfferedServer is one server Offer offers: the Server a client is told of, and the command that starts it.
type Offering ¶ added in v0.0.3
type Offering struct {
Starter
// contains filtered or unexported fields
}
Offering is a Starter offering servers that are commands in it, which Offer makes.
func Offer ¶ added in v0.0.3
func Offer(ws Starter, servers []OfferedServer, opts ...OfferOption) (*Offering, error)
Offer returns ws offering servers, each started from its command in ws, at its Dir with its Env, as Start starts any command: a ServerProvider for a workspace whose servers are programs installed in it. It is also a Starter, as ws is, so a program that starts commands of its own in the workspace can keep doing so. A server's Command.Stdin is refused when it starts, as Start refuses it, and its Command.Stream is replaced by StartServer's stderr. A name that is not ValidServerName is an error, and so is a name given twice, and so is a nil ws. A server's Secrets must each name a variable its Command.Env sets exactly once, to a value that is not empty, and a server that names any is refused unless WithOutputFilter is given: nothing would keep them out of its output. A nil option is skipped. The commands are copied: changing the slices given changes nothing Offer offers.
The filter covers the offered servers' own output and nothing else. An Offering embeds ws, so its Start and Exec run whatever they are given, unfiltered, as the same user as the servers; a client of a workspace that runs its commands (one served through mcpworkspace's NewProcesses answers both servers.start and process.start, and one served through remote to a principal holding exec runs its Exec and Start) can read a running server's environment, its secrets among it, from /proc/<pid>/environ. And a client that may write the workspace can change a server's code, config or PATH wherever they lie in it, and have the server print its secret in an encoding the filter does not know: no filter survives that. Offer secret-holding servers from a workspace the client can neither run commands in nor write, as dax's box host does with each server a unit in its own container, or serve the Offering through remote to a principal holding the servers scope alone.
func (*Offering) StartServer ¶ added in v0.0.3
StartServer starts the server called name from its command. With an OutputFilter, a server that names Secrets has its standard output and its standard error read through the filter.
What the filter gives of standard error is written to stderr by the time Wait or Close returns. Nothing reaches stderr that the filter has not seen whole and in order: when standard error cannot be filtered so, it is cut, for the rest of the server's life, after one line of this package's own saying why ("agentworkspace: <server>: standard error cut (<reason>)"), and nothing of the server's comes after it. It is cut when the filter falls behind by more than stderrBacklog (a megabyte; with what the filter is reading, the lag can reach about twice that), so the workspace's writes never wait on the filter; when the filter still holds some of it a short grace after the process has exited, so a filter that holds on does not hold up the host; and when the filter fails or panics. When it is stderr itself that stalls past the grace, Wait and Close return without it, and the notice is written once stderr takes writes again. Standard error the workspace writes after its process's Wait returns is dropped (io.ErrClosedPipe); Local writes all of it before. Standard output is not cut: a filter's failure there is the read's error.
type Output ¶
type Output struct {
Stdout, Stderr []byte
// ExitCode is the process's status; -1 when it was killed, or
// ended by its timeout or its context.
ExitCode int
// TimedOut reports that Command.Timeout ended it.
TimedOut bool
// Interrupted reports that the end of Exec's context ended it. The
// Output is then what the command wrote until its end, returned
// beside the context's error.
Interrupted bool
}
Output is how a process ended.
type OutputFilter ¶ added in v0.0.4
An OutputFilter is what a server's output is read through on its way out of an Offering: r is the server's standard output, or its standard error, and secrets are the values of the variables the server names as Secrets, by name, a copy of its own on each call. It is called twice for each start, once for each stream, and the two readers are read at once. The reader it returns must pass a line on as soon as it has it, since the MCP stream on standard output is a line at a time, and give what it holds back at r's end of file. A nil reader is refused: the start fails, and the server is ended.
A secret is replaced wherever its value appears, so a very short one ("1", "true") is replaced inside the server's own JSON-RPC too, which breaks the conversation rather than leak; nothing here holds a value to a length. A value holding a newline, such as a PEM key, is not found on its raw spelling by a filter that works a line at a time, as agentredact's secrets does (its JSON spellings, with \n escaped, are found); such values are taken, not refused.
agentredact's secrets module is the filter this is shaped for, each value replaced in every spelling a JSON encoder writes it in:
agentworkspace.WithOutputFilter(func(_ string, values map[string]string, r io.Reader) io.Reader {
return secrets.New(values).Reader(r, nil)
})
It is a backstop, not a guarantee: a value the server encodes otherwise (base64, split across messages) passes through.
type PortConn ¶ added in v0.0.11
type PortConn interface {
io.ReadWriteCloser
// CloseWrite ends what is sent, as a TCP half-close does: the port
// reads end of file, and this side may still read.
CloseWrite() error
}
PortConn is one byte stream to a TCP port in a workspace: what is written is sent to the port, and what the port sends is read.
type Process ¶
type Process interface {
// Stdin is the process's standard input. Closing it is end of file
// for the process.
Stdin() io.WriteCloser
// Stdout is the process's standard output. It reaches end of file
// once the process and everything it started have closed it.
Stdout() io.ReadCloser
// Wait waits for the process to exit and returns its status, -1 if
// a signal ended it. err is the context's error when the context or
// Command.Timeout ended it, and otherwise nil unless waiting itself
// failed; an exit status other than zero is not an error. Wait may
// be called any number of times and returns the same each time.
Wait() (status int, err error)
// Signal sends sig to the process and to everything it started, as
// far as the workspace's Reach goes. It is os.ErrProcessDone once
// the process has exited.
Signal(sig os.Signal) error
// Close ends the process, if it is still running, and everything it
// started, and releases the pipes. It first closes Stdin and gives
// the process a moment to exit, as the MCP stdio transport's
// shutdown asks, then climbs the ladder every end of a process
// climbs (the context's, Command.Timeout's, Exec's): it interrupts
// them (SIGINT), then terminates them (SIGTERM), then kills them
// (SIGKILL), a moment apart. It returns once the process has
// exited, and may be called more than once.
Close() error
}
Process is a process started in a workspace. Its methods are safe for concurrent use: one goroutine may read Stdout while another writes Stdin and a third waits.
type Reach ¶ added in v0.0.11
type Reach string
Reach is how far a Starter's end of a process reaches among what the process started. Every end (Process.Close, the context's, the timeout's, the workspace's Close, and Exec's) is meant to end every process the process started, directly or not, whatever group or session it moved to; a backend that cannot find some of them says which with its Reach, and a caller asks for it with ReachOf.
const ( // ReachGroup is the process's group alone: a descendant that // called setsid or setpgid, or that a shell's job control put in a // group of its own, outlives the end. ReachGroup Reach = "group" // ReachDescendants is the group and every descendant found through // parent links when a signal is sent, whatever its group or // session. A descendant whose parent exited before any signal was // sent has been given to another parent, and is not found. ReachDescendants Reach = "descendants" // ReachAll is every descendant, orphaned or not, as when the // backend ends a PID namespace or a cgroup whole. ReachAll Reach = "all" )
type Resizer ¶ added in v0.0.11
type Resizer interface {
Resize(size TerminalSize) error
}
Resizer is a Process that can change its terminal's size, as one started with Command.Terminal can; the process's foreground group is sent SIGWINCH.
type Server ¶ added in v0.0.3
type Server struct {
// Name is what the server is called, as ValidServerName holds it.
Name string
// Description says what the server is for, for a person choosing
// among them; empty when the workspace says nothing.
Description string
// Secrets says the workspace gives the server a secret (an
// OfferedServer that names Secrets); no name or value of one is
// told. It informs a listing and no more: a listing is the
// workspace's own word, and a caller must not take a server
// without the mark to hold no secret.
Secrets bool
}
Server is an MCP server a workspace offers: one it holds of its own, such as a server installed in a container's image or one a host runs beside a sandbox, rather than one the program names a command for.
type ServerProvider ¶ added in v0.0.3
type ServerProvider interface {
// Servers lists the servers the workspace offers, in a stable order.
Servers(ctx context.Context) ([]Server, error)
// StartServer starts the server called name and returns at once, as
// Starter's Start does, with the same lifetime: Process.Close, the
// end of ctx and the workspace's Close each end it and everything it
// started, and a caller that is done with it calls Close. Its
// standard input and output carry the MCP stream, and its standard
// error goes to stderr, or nowhere when stderr is nil. A name the
// workspace does not offer is an error that errors.Is ErrNoServer.
StartServer(ctx context.Context, name string, stderr io.Writer) (Process, error)
}
ServerProvider is a workspace that offers MCP servers of its own, each a stdio server it starts by name: the program lists them and connects to the ones it wants over the process's pipes, as it would to a server it started from a command. It is an interface of its own, as Starter is, so that a workspace offering none is still a Workspace; a caller asks for it with a type assertion. A client of a workspace elsewhere implements it too: mcpworkspace's, without being a Workspace, which is why it does not embed one, and remote's, when its principal holds the servers scope.
Which of the servers a session uses is the program's to decide: a workspace offering a server does not make the program connect to it.
type Starter ¶
type Starter interface {
Workspace
// Start starts cmd and returns at once. The process lives no longer
// than its owner: Process.Close, the end of ctx, cmd.Timeout and
// the workspace's Close each end it and everything it started, and
// when it exits by itself, what it started goes with it. A caller
// that is done with it calls Close, even after it exited.
Start(ctx context.Context, cmd Command) (Process, error)
}
Starter is a Workspace that can run a process for longer than one call, with pipes to it: an MCP server over stdio, a persistent shell. It is an interface of its own, as fs.ReadLinkFS is beside fs.FS, so that a workspace that only stores files, or a runtime whose API runs one command at a time, is still a Workspace; a caller asks for it with a type assertion, or calls Start.
type TerminalSize ¶ added in v0.0.11
type TerminalSize struct {
Rows, Cols uint16
}
TerminalSize is a terminal's size in character cells.
type Workspace ¶
type Workspace interface {
// Root is the absolute path of the working directory as the
// workspace's own processes see it: the checkout on this machine,
// /workspace in a container. It is what the model is told and what
// the session records as its cwd. It is "" when the workspace does
// not tell this side its root (a remote client whose principal may
// not read), never a partial path: a caller tests for "" before it
// joins a name with it or compares it with another.
Root() string
// FS reads the workspace's files. Opening a name never blocks: a
// FIFO or a device opens, or fails, at once, and a tool checks what
// it opened is a regular file. A name that leaves the workspace is
// an error that errors.Is ErrOutside. An FS that can tell a link
// from what it leads to implements fs.ReadLinkFS; one that cannot
// leaves a check that must follow links unable to, and so asking.
FS() fs.FS
// WriteFile creates or replaces a regular file, creating the
// directories above it. It refuses a FIFO or a device where the file
// is, and does not block on one.
WriteFile(ctx context.Context, name string, data []byte, perm fs.FileMode) error
// Remove deletes a file or an empty directory.
Remove(ctx context.Context, name string) error
// Env is the environment every process in the workspace starts
// with: what the workspace was given, a copy, with nothing added or
// removed. A workspace keeps no credential out of it; leaving the
// provider's key and other secrets out of what it is given is the
// program's.
Env() []string
// Exec runs one command to completion in a fresh process. An exit
// status other than zero is an Output, not an error. When
// Command.Timeout ends the command, Exec returns its Output with
// ExitCode -1 and TimedOut, and no error. When ctx ends first, Exec
// ends the command and everything it started, and returns the
// output collected so far, with ExitCode -1 and Interrupted, beside
// ctx.Err(); a ctx that has ended before the command starts runs
// nothing and gives no Output.
Exec(ctx context.Context, cmd Command) (*Output, error)
// Descriptor is what the session records about where the tools ran.
Descriptor() Descriptor
// Close releases the workspace. It is used until Close and not after.
Close() error
}
Workspace is where a session's tools act. Names given to it are fs.ValidPath names relative to Root; turning a path the model wrote into one is the tool's. Its methods are safe for concurrent use, and Root and Descriptor answer the same every time.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
lifetime
Package lifetime holds what Local's processes and the conformance tables that check them must agree on, so the two cannot drift.
|
Package lifetime holds what Local's processes and the conformance tables that check them must agree on, so the two cannot drift. |
|
portproc
Package portproc gives a stream to a port in a workspace the shape of a started process, so that a wire carries it on its process routes and methods (its input, its output, its close) rather than on a carrier of its own: a connect is a start whose process is the stream.
|
Package portproc gives a stream to a port in a workspace the shape of a started process, so that a wire carries it on its process routes and methods (its input, its output, its close) rather than on a carrier of its own: a connect is a start whose process is the stream. |
|
wireerr
Package wireerr holds what every wire's server sends of a refusal (RFC 0001 §15), so that remote and mcpworkspace cannot drift.
|
Package wireerr holds what every wire's server sends of a refusal (RFC 0001 §15), so that remote and mcpworkspace cannot drift. |
|
mcpworkspace
module
|
|
|
Package remote serves an agentworkspace.Workspace over HTTP, and is one at the other end: NewHandler serves any workspace, and Dial returns a workspace whose files, writes, environment, Exec and, when the served workspace is an agentworkspace.Starter, Start are the served one's, and, when it is an agentworkspace.ServerProvider, so are the MCP servers it offers.
|
Package remote serves an agentworkspace.Workspace over HTTP, and is one at the other end: NewHandler serves any workspace, and Dial returns a workspace whose files, writes, environment, Exec and, when the served workspace is an agentworkspace.Starter, Start are the served one's, and, when it is an agentworkspace.ServerProvider, so are the MCP servers it offers. |
|
tunnel
module
|
|
|
Package workspacetest is the conformance tables every agentworkspace backend runs: Run, the rules every Workspace keeps (confinement, nothing blocking on a FIFO, Env a copy, Exec, names that are not valid paths), RunStarter, the rules every Starter keeps (the pipes, the environment, the lifetimes, the refusals), and RunServerProvider, the rules every ServerProvider keeps (the list, the pipes, ErrNoServer, the lifetimes, standard error, secrets kept out of the output).
|
Package workspacetest is the conformance tables every agentworkspace backend runs: Run, the rules every Workspace keeps (confinement, nothing blocking on a FIFO, Env a copy, Exec, names that are not valid paths), RunStarter, the rules every Starter keeps (the pipes, the environment, the lifetimes, the refusals), and RunServerProvider, the rules every ServerProvider keeps (the list, the pipes, ErrNoServer, the lifetimes, standard error, secrets kept out of the output). |