Help us shape the future of Go! Take the 2026 Go Developer Survey

remote

package
v0.0.11 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 30 Imported by: 0

Documentation

Overview

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. It imports the standard library alone.

It is the primitive cut: every file operation and process crosses the wire on its own, and the tools, and the policy in front of them, run on the client's side. A sandbox that should run the tools themselves, and answer for each call, is cut at the tool calls instead (dax's executor, over MCP); this package is then what such an executor may run over inside the sandbox, or what a container's workspace serves.

Wire

Every route is under /v1 of the base URL. Bodies are JSON but for a file's content and a process's pipes, which are raw bytes streamed as they come. A name is a query's path, an fs.ValidPath name relative to the served Root; both halves refuse any other, one that leaves as agentworkspace.ErrOutside.

GET    /v1/info                              read or servers  root, the served descriptor, links, the bounds, the served reach; Start with exec, Servers with servers, Env with exec
GET    /v1/fs/stat?path=N                    read
GET    /v1/fs/lstat?path=N                   read     unsupported when the served FS cannot read links
GET    /v1/fs/readlink?path=N                read     ditto
GET    /v1/fs/readdir?path=N                 read     at most the listing bound of entries
GET    /v1/fs/file?path=N                    read     a regular file's content, streamed
PUT    /v1/fs/file?path=N&perm=M             write    the body is the content, at most the write bound; perm's permission bits alone
DELETE /v1/fs/file?path=N                    write
POST   /v1/exec                              exec     {args, dir, env, stdin, timeoutMs, stream, key} -> NDJSON frames
POST   /v1/exec/{key}/cancel                 exec     ends the exec running under key as its context's end does; its reply carries the output so far
GET    /v1/servers                           servers  -> {servers: [{name, description, secrets}]}, in the served order; unsupported when it is no ServerProvider
POST   /v1/sessions                          either   -> {id, leaseMs}
POST   /v1/sessions/{s}/renew                either
DELETE /v1/sessions/{s}                      either   ends every process it started
POST   /v1/sessions/{s}/processes            exec     {args, dir, env, timeoutMs, stderr, terminal: {rows, cols}, key} -> {id}, the key
POST   /v1/sessions/{s}/connect              exec     {port, key} -> {id}, the key: a stream to port on the served workspace's loopback, a process of the session whose stdin, stdout and close are the stream's
POST   /v1/sessions/{s}/servers/{n}/start    servers  {stderr, key} -> {id}, the key: the server called n, a process of the session
POST   /v1/sessions/{s}/processes/{p}/stdin  its own  the body, streamed to its input; its end is end of file
GET    /v1/sessions/{s}/processes/{p}/stdout its own  its output, streamed
GET    /v1/sessions/{s}/processes/{p}/stderr its own  its standard error, when the start asked for it
GET    /v1/sessions/{s}/processes/{p}/wait   its own  a long poll -> {status, ended?, error?}, the same each time; one at a time: another while one waits is invalid, ask again
POST   /v1/sessions/{s}/processes/{p}/signal its own  {signal: "TERM"}, a name: HUP INT QUIT KILL USR1 USR2 PIPE ALRM TERM CONT STOP TSTP WINCH
POST   /v1/sessions/{s}/processes/{p}/cancel its own  ends it as its context's end does
POST   /v1/sessions/{s}/processes/{p}/resize its own  {rows, cols}: its terminal's size; unsupported for one with none
DELETE /v1/sessions/{s}/processes/{p}        its own  ends it as Process.Close does -> {status, ...}

A session's routes take exec or servers, either one ("either"). A process's routes take the scope it was started under ("its own"): exec for a command's, servers for a server's, so a principal holding one of them is refused a process the other started (ErrForbidden), though both are in its session. A session belongs to the principal that opened it, by Name, and not to a scope: its renewal and its close take either scope, and its close, or its lapse, ends every process in it, whichever scope started each. A Name should therefore carry one set of scopes; given several, any of its tokens renews and closes the sessions the Name opens. Info's Start and Servers each say what the asking principal may start here: Start that the served workspace is a Starter and the principal holds exec, Servers that it is a ServerProvider and the principal holds servers.

A body is application/json, or application/octet-stream for a file's content and a process's input, and a route refuses any other type (415). Exec's reply is NDJSON (application/x-ndjson), one frame a line: {"data": base64} for Command.Stream's output as it arrives, at most 32 KiB of it a frame, when the request asked for a stream, then one {"output": {...}} or one {"error": {...}}, which is the last. A refusal before the command runs is an HTTP error. An exec with a key (chosen at random, as a start's) is the principal's to cancel by it: the server sends the reply's header once the key is taken, a cancel ends the command and what it started as a hang-up would, and the reply goes on to its output frame, with interrupted set and the output so far, which a hang-up loses. A start carries a key the client chose at random, which becomes the process's id: a start sent again is answered with the same process, one whose reply was lost is closed by its key, and a key whose process closed starts nothing again; a server's start is keyed so too, and a key names one start, of one command or one server. Every error is {"error": {"code", "message"}}; the code is the sentinel's (agentworkspace.ErrOutside, fs.ErrNotExist, fs.ErrExist, fs.ErrPermission, fs.ErrInvalid, errors.ErrUnsupported, os.ErrProcessDone, fs.ErrClosed, agentworkspace.ErrNoServer, and this package's own), which the client maps back, so errors.Is holds on the client as it does on the served workspace. A mapped error's message is the system's or the sentinel's words, never the served error's own text, which may name a path on the server's machine; any other error's is its text, the served workspace's words, at most MaxErrorBytes.

Scopes

This package ships no authentication: it is the capability layer, and who a request is from is the program's to decide. Every request is put to the Authenticator given with WithAuthenticator, the program's hook, whatever its route, which answers with the Principal it is from and the scopes it holds; without one, and without WithInsecureNoAuth, every request is refused. A refusal as unauthenticated is a 401 that carries the program's challenge, its WWW-Authenticate, when it gives one (WithChallenge), and none otherwise: the scheme is the program's to name. A principal's Name is what keeps its sessions from another's, and one with an empty Name is refused as unauthenticated. read covers the info route and the file system's reads; write, WriteFile and Remove; exec, Exec, a command's start, the sessions and the processes it started, and the environment, which the info route gives a principal without exec none of: such a client's Env is empty and it is no Starter. exec is never implied by read, write or servers. The info route names the principal's scopes too, and a client's Scopes are those it named at Dial.

servers covers the MCP servers a served agentworkspace.ServerProvider offers: their list, a server's start, the sessions they run in and the routes of the processes it started so, and nothing else. It does not imply exec, nor exec it, and it needs no read: the info route answers it too, with nothing but the root's last element, the descriptor without its Host, the bounds and that servers are offered; the client takes a root that is not absolute as withheld, and its Root, and its Served and Descriptor roots, are "", never a partial path. A principal holding servers alone dials, lists and starts the offered servers and talks to them over their pipes, and reads no file (its client's FS, WriteFile and Remove are ErrForbidden), runs no command (no Exec, no Start, no process exec started) and is told no environment; its client is an agentworkspace.ServerProvider and no Starter. That is how a host grants a client its servers and nothing else, and a server that holds secrets is offered safely only to a principal holding servers alone. exec exposes its environment: an agentworkspace.Offering embeds its Starter, so a client that may run commands as the servers' user reads a running server's environment from /proc (see agentworkspace.Offer). write defeats any filter: a client that may write the workspace can change the 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. read reads whatever a server keeps in the workspace. A server is started by the served workspace's StartServer, on the serving side, so an Offering's OutputFilter reads its output there, before a byte of it is sent, and the secrets it names never cross the wire.

The client sends no credentials of its own: the program's go in the http.Client it gives WithHTTPClient (a Transport that sets the Authorization header, a client certificate), and nothing of them is put in a URL, an error or a Descriptor. TLS, issuing and checking credentials and deciding by path are the program's: the handler is a plain http.Handler it mounts.

This API has no browser clients, and a page the user opens must not reach it: every handler refuses a request carrying an Origin, or a Sec-Fetch-Site other than same-origin or none, and a body of another type than its route's, which a page cannot send without a preflight nothing answers. One made WithInsecureNoAuth, for a loopback listener the program alone dials, answers only a Host that is localhost or a loopback address (127.0.0.0/8, ::1, any port), so a name a page rebinds to 127.0.0.1 is refused too.

Lifetimes

HTTP has no connection to end with the client, so the client opens a session at its first Start or StartServer, and renews it every third of its lease (DefaultLease); a Dial takes nothing on the server. A session that goes a lease unrenewed lapses, and Client.Close closes it; either way the server closes every process the session started. A client that goes a whole lease without a renewal that succeeded takes the session as lost too: its processes' Wait returns -1 and an error that is ErrNoSession, their pipes fail with it, and the next start opens another session. A started process, a server among them, lives no longer than its owner: Process.Close, the end of Start's context or StartServer's (the client asks the server to end it as its context's end would), Command.Timeout (kept by the server) and Client.Close each end it and everything it started. A process's standard error, when its start asked for it, is answered as soon as the server has claimed its stream, and Process.Close is sent once the client has that answer (or after a grace, two seconds), so a Command.Stream gets what the process writes on its way out however soon Close follows Start; the server also serves a closed process's standard error to a stream that comes after the close, until a grace after its exit. Exec's process lives no longer than its request. Handler.Close ends every session's processes and every Exec, and returns once they have ended. Session and process ids are random (crypto/rand's Text), and a process is reached only through the session that started it, by the principal that opened that: another's reads as no such session.

Bounds

A JSON body is at most MaxRequestBytes; a WriteFile's data and an Exec's stdin at most the write bound (DefaultMaxWriteBytes, WithMaxWriteBytes); a listing at most DefaultMaxDirEntries entries (WithMaxDirEntries), of which the server reads one past the bound and no more; a handler holds at most DefaultMaxSessions sessions (WithMaxSessions), a principal at most DefaultMaxSessionsPerPrincipal of them (WithMaxSessionsPerPrincipal), and a session at most DefaultMaxProcesses processes (WithMaxProcesses), its commands and its servers together; a handler serves at most DefaultMaxTransfers Execs and reads and writes of a file's content at once (WithMaxTransfers); an Exec without a Stream gives back at most DefaultMaxExecOutputBytes of each stream (WithMaxExecOutputBytes), and past it the command has run and Exec is ErrLimit, its output not sent (the served workspace's own buffering of it is its own); a process's standard error is held StderrBytes deep for a client behind on it, and a Command.Stream that takes no more is left behind five seconds after the process ends; a client reads at most MaxReplyBytes of a JSON reply, and at most what the server's bounds allow of one of Exec's frames. Over a bound is ErrLimit. The info route tells the client the bounds the server keeps, and the client sends no more.

Index

Constants

View Source
const (
	// DefaultLease is how long a session lives unrenewed (WithLease).
	// A client renews it every third of that.
	DefaultLease = 30 * time.Second
	// DefaultMaxSessions bounds the sessions open at once on one
	// Handler (WithMaxSessions).
	DefaultMaxSessions = 64
	// DefaultMaxSessionsPerPrincipal bounds the sessions one principal
	// has open at once (WithMaxSessionsPerPrincipal).
	DefaultMaxSessionsPerPrincipal = 8
	// DefaultMaxProcesses bounds the processes open at once in one
	// session (WithMaxProcesses).
	DefaultMaxProcesses = 32
	// DefaultMaxWriteBytes bounds a WriteFile's data and an Exec's
	// Command.Stdin (WithMaxWriteBytes).
	DefaultMaxWriteBytes = 32 << 20
	// DefaultMaxDirEntries bounds one listing (WithMaxDirEntries).
	DefaultMaxDirEntries = 10000
	// DefaultMaxExecOutputBytes bounds each of the standard output and
	// the standard error an Exec without a Stream sends back
	// (WithMaxExecOutputBytes).
	DefaultMaxExecOutputBytes = 16 << 20
	// DefaultMaxTransfers bounds the Execs and the reads and writes of a
	// file's content served at once (WithMaxTransfers).
	DefaultMaxTransfers = 256
)

The defaults of the bounds a Handler keeps, each changed by its option.

View Source
const (
	// MaxRequestBytes bounds the JSON body of every route that takes
	// one, and, added to the base64 of the write bound, an Exec's: room
	// for a command line as long as Linux takes (ARG_MAX, 2 MiB) and
	// its environment, escaped.
	MaxRequestBytes = 4 << 20
	// MaxErrorBytes bounds an error's message on the wire.
	MaxErrorBytes = 512
	// StderrBytes is how much of a started process's standard error a
	// server holds that its client has not read yet; what does not fit
	// is dropped, so that the process never waits on a slow client's
	// Command.Stream.
	StderrBytes = 256 << 10
	// MaxReplyBytes bounds a JSON reply a client reads but Exec's
	// frames, each of which the client bounds by what the server says
	// it sends: a data frame at most a chunk, the Output twice the
	// base64 of the server's output bound.
	MaxReplyBytes = 16 << 20
)

The bounds that are not options.

Variables

View Source
var (
	// ErrUnauthenticated is a request no principal was resolved for: no
	// authenticator, or one the authenticator refused.
	ErrUnauthenticated = errors.New("remote: unauthenticated")
	// ErrForbidden is a request whose principal lacks the scope its
	// route needs.
	ErrForbidden = errors.New("remote: the principal lacks the scope this needs")
	// ErrLimit is a request over one of the server's bounds: a body too
	// large, too many sessions, too many processes in one, a listing too
	// long.
	ErrLimit = errors.New("remote: over a bound of the server's")
	// ErrNoSession is a session that lapsed, was closed, or is another
	// principal's.
	ErrNoSession = errors.New("remote: no such session")
	// ErrNoProcess is a process id the session did not start, or closed.
	ErrNoProcess = errors.New("remote: no such process in this session")
)

The errors of the wire's own, which the client returns as they are and a served workspace's error is never mapped to.

Functions

This section is empty.

Types

type Authenticator

type Authenticator func(r *http.Request) (Principal, error)

Authenticator resolves the principal a request is from, and so what it may do: it is the program's authentication, which this package does not implement (no tokens, no certificates, no sessions of a login), only asks for. An error refuses the request as ErrUnauthenticated; its text is not sent.

type Bounds added in v0.0.8

type Bounds struct {
	MaxSessions             int   // WithMaxSessions
	MaxSessionsPerPrincipal int   // WithMaxSessionsPerPrincipal
	MaxProcesses            int   // WithMaxProcesses, in one session
	MaxTransfers            int   // WithMaxTransfers
	MaxWriteBytes           int64 // WithMaxWriteBytes
	MaxExecOutputBytes      int64 // WithMaxExecOutputBytes
	MaxDirEntries           int   // WithMaxDirEntries
}

Bounds are a Handler's bounds, each its option's or its default: what a program that serves the handler over a wire of its own checks that wire's limits against (tunnel does, against its streams). The lease is Lease's, and the bounds no option changes (MaxRequestBytes, StderrBytes, MaxReplyBytes, MaxErrorBytes) are constants, not repeated here.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is a workspace served by a Handler, reached over HTTP. Its methods are safe for concurrent use.

func (*Client) Close

func (c *Client) Close() error

Close ends every process the client started, as Process.Close does, closes its session, which ends whatever it did not reach, and ends every request in flight. It may be called more than once.

func (*Client) Descriptor

func (c *Client) Descriptor() agentworkspace.Descriptor

Descriptor names the remote: KindRemote, the base URL with no userinfo, query or fragment as its Ref, and the served Root.

func (*Client) Env

func (c *Client) Env() []string

Env is the served workspace's Env, a copy, as it was at Dial; empty for a principal without exec, whom the server does not tell it.

func (*Client) Exec

Exec runs one command in the served workspace. The end of ctx ends it and everything it started, as on the served workspace: the client asks the server to end it, by the key it sent, and reads on, so the Output it returns beside ctx.Err(), with ExitCode -1 and Interrupted, holds what the command wrote until its end. A server that does not answer the cancel, or does not answer it within a bound, is hung up on, and the Output is then empty. Close ends it too, by hanging up. Command.Timeout is sent, and the server ends the process at it. Command.Stream gets the output as the server sends it; when writing to it fails, the command is ended and Exec returns that error.

func (*Client) FS

func (c *Client) FS() fs.FS

FS reads the served workspace's files. It implements fs.StatFS and fs.ReadDirFS, and fs.ReadLinkFS when the served FS does: one that cannot tell a link from what it leads to stays a plain FS here too, so a check that must follow links asks instead of allowing. Open stats the name, following links inside the workspace, and never blocks on what is there: a regular file's content is streamed on its first Read, and a FIFO or a device opens, its Stat answers, and a Read of it is an error. Each request but a content's stream is bounded by WithTimeout.

func (*Client) Reach added in v0.0.11

func (c *Client) Reach() agentworkspace.Reach

Reach is how far the served workspace's end of a process reaches, as the server said it at Dial (agentworkspace.ReachOf on the served workspace), and "" when it said none or one this client does not know. A served Local asks its system at each call; the client keeps what it was told.

func (*Client) Remove

func (c *Client) Remove(ctx context.Context, name string) error

Remove deletes a file or an empty directory in the served workspace.

func (*Client) Root

func (c *Client) Root() string

Root is the served workspace's Root, or "" for a principal without read, whom the server does not tell it: the info route then gives the root's last element alone, which is no path and is not passed on.

func (*Client) Scopes added in v0.0.8

func (c *Client) Scopes() []Scope

Scopes are the scopes the server said the principal holds when it answered Dial, a copy, in the server's order: what the program's Authenticator granted the credentials then. They are not asked again, so a grant changed since is not in them, and the server checks every request against the principal it resolves for it, whatever they say. They are never empty: Dial refuses a description that names none (a Handler's info route itself takes read or servers). They tell a program what a refusal will be before it asks: without ScopeWrite, WriteFile and Remove are ErrForbidden.

func (*Client) Served

func (c *Client) Served() agentworkspace.Descriptor

Served is the served workspace's own Descriptor, its Root as Root gives it: "" when it is withheld.

func (*Client) WriteFile

func (c *Client) WriteFile(ctx context.Context, name string, data []byte, perm fs.FileMode) error

WriteFile creates or replaces a regular file in the served workspace, at most the server's write bound of it (ErrLimit).

type DialOption

type DialOption func(*dialConfig)

A DialOption configures Dial.

func WithHTTPClient

func WithHTTPClient(hc *http.Client) DialOption

WithHTTPClient sends every request through hc, which is where the program's credentials go: a Transport that sets a request's Authorization header, or a TLS config with a client certificate. This package sends none of its own, and puts nothing of hc's in a URL, an error or a Descriptor. hc must set no Timeout, which would cut Exec's frames and a process's streams short; the client bounds the requests that should be answered at once itself (WithTimeout), and should follow no redirect, which would send the credentials on. The default is a client of its own over http.DefaultTransport that follows no redirect and sends no credentials.

func WithTimeout

func WithTimeout(d time.Duration) DialOption

WithTimeout bounds each request that takes no context and should be answered at once (the file system's, a session's opening and renewal, a process's start, signal and close); the default is 30 seconds. A stream, a wait and an Exec are bounded by their contexts alone.

type Handler

type Handler struct {
	// contains filtered or unexported fields
}

Handler serves one workspace over HTTP, every route under /v1 of the URL it is mounted at (behind http.StripPrefix when that has a path). It is a plain http.Handler: TLS, and the timeouts of the server it is mounted on, are the program's, and a server with a ReadTimeout or a WriteTimeout cuts the streams (Exec's frames, a process's pipes, a wait) short.

Every request is authenticated first, whatever its route, and refused without an authenticator unless the handler was made with WithInsecureNoAuth: it fails closed. Before that, a request that looks like a browser page's is refused in every mode (it carries an Origin, or a Sec-Fetch-Site other than same-origin or none: this API has no browser clients), and a route that takes a body takes only its own type (application/json, or application/octet-stream for a file's content and a process's input), which a page cannot send without a preflight. Closing the handler ends every session's processes and every Exec; the served workspace is the program's to close.

func NewHandler

func NewHandler(ws agentworkspace.Workspace, opts ...Option) (*Handler, error)

NewHandler serves ws. It refuses a nil workspace, a bound that is not positive, and both an authenticator and WithInsecureNoAuth.

A program that wraps the handler's http.ResponseWriter (middleware that logs or measures) gives the wrapper an Unwrap method returning the server's, as http.ResponseController asks: through it the handler flushes a stream, and cuts a closed process's streams by their deadlines, which keeps what a process holds within its bounds whatever its client does.

func (*Handler) Bounds added in v0.0.8

func (h *Handler) Bounds() Bounds

Bounds are the bounds h was made with.

func (*Handler) Close

func (h *Handler) Close() error

Close ends every session, and every process each started, and every Exec in flight, and refuses every request after; it returns once they have all ended, with everything they started. It does not close the served workspace.

func (*Handler) Lease added in v0.0.6

func (h *Handler) Lease() time.Duration

Lease is how long a session lives without a renewal: WithLease's, or DefaultLease.

func (*Handler) ServeHTTP

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP refuses r if it looks like a browser page's, authenticates it, whatever its route, and serves it.

type Option

type Option func(*Handler)

An Option configures NewHandler.

func WithAuthenticator

func WithAuthenticator(a Authenticator) Option

WithAuthenticator resolves each request's principal with a, the program's authentication.

func WithChallenge added in v0.0.8

func WithChallenge(value string) Option

WithChallenge sends value as the WWW-Authenticate header of every 401 the handler writes: a request the Authenticator refused, one that named no principal, every one without an Authenticator, and a served workspace's error that is ErrUnauthenticated. value is the program's challenge, its scheme and any parameters ("Bearer", or `Bearer realm="workspace"`), sent as given. Without it, or with "", a 401 carries no challenge: the authentication is the program's, and so is naming its scheme.

func WithInsecureNoAuth

func WithInsecureNoAuth() Option

WithInsecureNoAuth serves every request, unauthenticated, as a principal named "insecure" that holds every scope. For tests, and for a program that has decided nothing reaches the handler but itself (a loopback listener it alone dials), explicitly. Such a handler answers only a request whose Host is a loopback name or address (localhost, 127.0.0.0/8, ::1, with any port), so a page that rebinds a name of its own to 127.0.0.1 is refused; the browser checks every handler makes (no Origin, no cross-site Sec-Fetch-Site, typed bodies) keep a page from sending it a request at all.

func WithLease

func WithLease(d time.Duration) Option

WithLease is how long a session lives without a renewal; the default is DefaultLease.

func WithMaxDirEntries

func WithMaxDirEntries(n int) Option

WithMaxDirEntries bounds one listing: a directory holding more is ErrLimit. The default is DefaultMaxDirEntries.

func WithMaxExecOutputBytes

func WithMaxExecOutputBytes(n int64) Option

WithMaxExecOutputBytes bounds each of the standard output and the standard error an Exec without a Command.Stream sends back; the default is DefaultMaxExecOutputBytes. Past it the command has run, and its Exec is ErrLimit, its output not sent: one whose output may be larger gives a Stream. The served workspace's own buffering of that output is its own (Local's is not bounded).

func WithMaxProcesses

func WithMaxProcesses(n int) Option

WithMaxProcesses bounds the processes open at once in one session, a closed one counted until it has exited and its streams have ended (one whose standard error was asked for and not streamed, until a grace after its exit); the default is DefaultMaxProcesses.

func WithMaxSessions

func WithMaxSessions(n int) Option

WithMaxSessions bounds the sessions open at once; the default is DefaultMaxSessions.

func WithMaxSessionsPerPrincipal

func WithMaxSessionsPerPrincipal(n int) Option

WithMaxSessionsPerPrincipal bounds the sessions one principal (by Name) has open at once, so that one cannot take every slot; the default is DefaultMaxSessionsPerPrincipal.

func WithMaxTransfers added in v0.0.6

func WithMaxTransfers(n int) Option

WithMaxTransfers bounds the transfers served at once: Execs and a file's content read or written, each of which holds its request open as long as its body streams. One past it is ErrLimit. The sessions' own routes (a renewal, a process's start, pipes, wait, signal, cancel and close) are bounded by the sessions and their processes, and are not counted. The default is DefaultMaxTransfers.

func WithMaxWriteBytes

func WithMaxWriteBytes(n int64) Option

WithMaxWriteBytes bounds a WriteFile's data and an Exec's stdin; the default is DefaultMaxWriteBytes.

type Principal

type Principal struct {
	Name   string
	Scopes []Scope
}

Principal is who a request is from, as an Authenticator resolved it, and what they may do. A session, and every process started in it, belongs to the principal's Name: another principal's request names no such session. The Name is the session's owner, so a Name should carry one set of Scopes: an Authenticator that gives one Name several sets (two tokens, say, one holding exec and one servers) lets each renew and close the sessions the Name opens, and a close ends every process in the session, whichever scope started it.

func PrincipalFrom

func PrincipalFrom(ctx context.Context) (Principal, bool)

PrincipalFrom is the principal a Handler resolved for the request whose context ctx is.

func (Principal) Has

func (p Principal) Has(s Scope) bool

Has reports whether p holds s.

type Scope

type Scope string

Scope is what a principal may do through a Handler.

const (
	// ScopeRead reads: the info route and every read of the files
	// (stat, lstat, readlink, readdir, a file's content).
	ScopeRead Scope = "read"
	// ScopeWrite writes and removes files.
	ScopeWrite Scope = "write"
	// ScopeExec runs anything: Exec, a process's start, the sessions,
	// the routes of the processes it started, and the workspace's
	// environment, which the info route gives only to a principal
	// holding it. It is never implied by read, write or servers.
	ScopeExec Scope = "exec"
	// ScopeServers starts the MCP servers the workspace offers (an
	// agentworkspace.ServerProvider): it lists them, starts one by name,
	// opens and renews the sessions they run in, uses the routes of the
	// processes it started so, and reads the info route, and nothing
	// else. It runs no command of the client's: no Exec, no process's
	// start, no route of a process exec started, and no environment;
	// without read it reads no file. It does not imply exec, nor exec
	// it, and needs no read to dial.
	//
	// Granted alone, it is how a host gives a client the servers it
	// offers and nothing else, and a secret-holding server is offered
	// safely only to a principal holding servers alone. With exec, the
	// client runs commands as the servers' user and reads a running
	// server's environment from /proc (see agentworkspace.Offer). With
	// write, it can change the server's code, config or PATH wherever
	// they lie in the workspace, and have the server print its secret in
	// an encoding no OutputFilter looks for. With read, it reads
	// whatever a server keeps in the workspace.
	ScopeServers Scope = "servers"
)

type ServerClient added in v0.0.7

type ServerClient struct{ *Client }

ServerClient is a Client that may start the MCP servers the served workspace offers and no process of its own: the served workspace is an agentworkspace.ServerProvider and the principal holds servers, and either the served workspace is no Starter or the principal lacks exec.

func (*ServerClient) Servers added in v0.0.7

func (s *ServerClient) Servers(ctx context.Context) ([]agentworkspace.Server, error)

Servers lists the MCP servers the served workspace offers, in the order it lists them. It is bounded by ctx and by WithTimeout.

func (*ServerClient) StartServer added in v0.0.7

func (s *ServerClient) StartServer(ctx context.Context, name string, stderr io.Writer) (agentworkspace.Process, error)

StartServer starts the server called name in the served workspace, in the client's session, and returns at once, as StarterClient's Start does, with the same lifetime: Process.Close, the end of ctx, the client's Close and the session's loss each end it and everything it started. Its standard input and output carry the MCP stream, and its standard error, when stderr is set, is streamed there as Command.Stream is for Start. The served workspace starts it, so what an agentworkspace.Offering's OutputFilter keeps out of its output is kept out before it is sent, and its secrets are never told to this side. A name the served workspace does not offer is an error that errors.Is agentworkspace.ErrNoServer.

type StarterClient

type StarterClient struct{ *Client }

StarterClient is a Client that may start processes: the served workspace is an agentworkspace.Starter and the principal holds exec.

func (*StarterClient) Connect added in v0.0.11

func (s *StarterClient) Connect(ctx context.Context, port int) (agentworkspace.PortConn, error)

Connect opens a stream to port on the served workspace's loopback (agentworkspace.Connector), carried as a process of the client's session: writes go to the port as its input's stream, reads come from its output's, CloseWrite closes its input and Close closes it. ctx bounds the connecting alone. A served workspace that cannot connect is errors.ErrUnsupported.

func (*StarterClient) Start

Start starts cmd in the served workspace, in the client's session (opened now, if this is the first Start or the last session was lost), and returns at once. The process lives no longer than its owner: Process.Close, the end of ctx, cmd.Timeout (sent, and kept by the server), the client's Close and the session's loss each end it and everything it started. Its standard input is a stream sent as it is written, opened on the first write or close; its standard output is a stream opened on the first read; and its standard error, when cmd.Stream is set, is streamed there in the background, StderrBytes of it held by the server while the stream is behind and the rest dropped. A stream cut short is not resumed. cmd.Stdin is refused; write to the process's input instead.

The start is sent with a key of the client's choosing, which names the process: when its reply is lost, the client closes the process by the key, so nothing runs that no one can name.

type StarterServerClient added in v0.0.7

type StarterServerClient struct{ *Client }

StarterServerClient is a Client that may do both: start processes, as a StarterClient, and the servers the served workspace offers, as a ServerClient.

func (*StarterServerClient) Connect added in v0.0.11

Connect is StarterClient's.

func (*StarterServerClient) Servers added in v0.0.7

Servers is ServerClient's.

func (*StarterServerClient) Start added in v0.0.7

Start is StarterClient's.

func (*StarterServerClient) StartServer added in v0.0.7

func (s *StarterServerClient) StartServer(ctx context.Context, name string, stderr io.Writer) (agentworkspace.Process, error)

StartServer is ServerClient's.

type Workspace

type Workspace interface {
	agentworkspace.Workspace
	// Served is the served workspace's own Descriptor, as against
	// Descriptor, which names this one: the remote at the base URL.
	Served() agentworkspace.Descriptor
	// Scopes are the scopes the server said, at Dial, the principal
	// holds; never empty.
	Scopes() []Scope
}

Workspace is what Dial returns: an agentworkspace.Workspace at the other end of the wire, which also tells the served workspace's own descriptor. It is a *Client; a *StarterClient when the client may start processes; a *ServerClient when it may start the servers the served workspace offers; or a *StarterServerClient when it may do both: so that a type assertion to agentworkspace.Starter, and one to agentworkspace.ServerProvider, each tells the truth.

func Dial

func Dial(ctx context.Context, baseURL string, opts ...DialOption) (Workspace, error)

Dial connects to the Handler at baseURL: it asks for the served workspace's description, and takes nothing on the server. ctx bounds the dial alone.

The workspace returned is an agentworkspace.Starter (a *StarterClient) when the served workspace can start processes and the principal holds exec; an agentworkspace.ServerProvider (a *ServerClient) when the served workspace offers MCP servers and the principal holds servers; a *StarterServerClient, both, when both hold; and a *Client otherwise. Each opens the session its processes live in at its first Start or StartServer, and renews it in the background until Close; a session lost (lapsed on the server, or a whole lease unrenewed) ends its processes on this side too, and the next start opens another. Its Root is the served workspace's, its Descriptor is KindRemote with the base URL as its Ref, and Served is the served workspace's own. A base URL with userinfo is refused (credentials go in the HTTP client, WithHTTPClient); its query and fragment are dropped.

Jump to

Keyboard shortcuts

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