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
- Variables
- type Authenticator
- type Bounds
- type Client
- func (c *Client) Close() error
- func (c *Client) Descriptor() agentworkspace.Descriptor
- func (c *Client) Env() []string
- func (c *Client) Exec(ctx context.Context, cmd agentworkspace.Command) (*agentworkspace.Output, error)
- func (c *Client) FS() fs.FS
- func (c *Client) Reach() agentworkspace.Reach
- func (c *Client) Remove(ctx context.Context, name string) error
- func (c *Client) Root() string
- func (c *Client) Scopes() []Scope
- func (c *Client) Served() agentworkspace.Descriptor
- func (c *Client) WriteFile(ctx context.Context, name string, data []byte, perm fs.FileMode) error
- type DialOption
- type Handler
- type Option
- func WithAuthenticator(a Authenticator) Option
- func WithChallenge(value string) Option
- func WithInsecureNoAuth() Option
- func WithLease(d time.Duration) Option
- func WithMaxDirEntries(n int) Option
- func WithMaxExecOutputBytes(n int64) Option
- func WithMaxProcesses(n int) Option
- func WithMaxSessions(n int) Option
- func WithMaxSessionsPerPrincipal(n int) Option
- func WithMaxTransfers(n int) Option
- func WithMaxWriteBytes(n int64) Option
- type Principal
- type Scope
- type ServerClient
- type StarterClient
- type StarterServerClient
- func (s *StarterServerClient) Connect(ctx context.Context, port int) (agentworkspace.PortConn, error)
- func (s *StarterServerClient) Servers(ctx context.Context) ([]agentworkspace.Server, error)
- func (s *StarterServerClient) Start(ctx context.Context, cmd agentworkspace.Command) (agentworkspace.Process, error)
- func (s *StarterServerClient) StartServer(ctx context.Context, name string, stderr io.Writer) (agentworkspace.Process, error)
- type Workspace
Constants ¶
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
func (c *Client) Exec(ctx context.Context, cmd agentworkspace.Command) (*agentworkspace.Output, error)
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 ¶
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) Root ¶
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
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.
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) Close ¶
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.
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
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 ¶
WithLease is how long a session lives without a renewal; the default is DefaultLease.
func WithMaxDirEntries ¶
WithMaxDirEntries bounds one listing: a directory holding more is ErrLimit. The default is DefaultMaxDirEntries.
func WithMaxExecOutputBytes ¶
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 ¶
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 ¶
WithMaxSessions bounds the sessions open at once; the default is DefaultMaxSessions.
func WithMaxSessionsPerPrincipal ¶
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
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 ¶
WithMaxWriteBytes bounds a WriteFile's data and an Exec's stdin; the default is DefaultMaxWriteBytes.
type Principal ¶
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 ¶
PrincipalFrom is the principal a Handler resolved for the request whose context ctx is.
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 ¶
func (s *StarterClient) Start(ctx context.Context, cmd agentworkspace.Command) (agentworkspace.Process, error)
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
func (s *StarterServerClient) Connect(ctx context.Context, port int) (agentworkspace.PortConn, error)
Connect is StarterClient's.
func (*StarterServerClient) Servers ¶ added in v0.0.7
func (s *StarterServerClient) Servers(ctx context.Context) ([]agentworkspace.Server, error)
Servers is ServerClient's.
func (*StarterServerClient) Start ¶ added in v0.0.7
func (s *StarterServerClient) Start(ctx context.Context, cmd agentworkspace.Command) (agentworkspace.Process, error)
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 ¶
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.