Documentation
¶
Overview ¶
Package rpc is Godwit's line-delimited JSON-RPC 2.0 transport: a strict subset of the spec (positive integer IDs, positional params, no batches), reflective dispatch over a target's methods, and cancellable streams.
Control messages, all notifications:
$/stream [S, event] server → client, one stream event $/stream.end [S] | [S, error] $/stream.cancel [S] client → server $/cancel [id] client → server, cancel an in-flight request
Index ¶
- Constants
- Variables
- func DecodeDetail[V any](raw json.RawMessage) (*V, error)
- func MCPAllowed(method string, params []json.RawMessage) bool
- func RegisterType[T error](c *Codec, code string, encode func(T) any, ...)
- func ServeConn(ctx context.Context, rwc net.Conn, d *Dispatcher, opts ConnOptions) error
- type Client
- func (c *Client) Call(ctx context.Context, method string, result any, args ...any) error
- func (c *Client) Close() error
- func (c *Client) Stream(ctx context.Context, method string, args ...any) (events <-chan json.RawMessage, cancel func(), err error)
- func (c *Client) StreamErr(events <-chan json.RawMessage) error
- type Codec
- type ConnOptions
- type Dispatcher
- type RemoteError
- type Server
- type ServerOptions
- type WireError
- type WireErrorData
Constants ¶
const ( CodeParse = -32700 CodeInvalidRequest = -32600 CodeMethodNotFound = -32601 CodeInvalidParams = -32602 CodeInternal = -32603 CodeTooMany = -32000 CodeForbidden = -32001 CodeUnauthenticated = -32002 )
Error codes.
const ( RoleFull = "full" // every exposed method, for the user's own programs RoleMCP = "mcp" // read and draft only, for godwit mcp )
Roles a connection declares in hello.
const ( SocketName = "godwit.sock" TokenName = "api-token" )
File names under the data directory.
const CodeApp = -32003
CodeApp is the wire code of a registered (application) error.
const StreamBuffer = 256
StreamBuffer is how many unread events a client stream holds before the client cancels it.
Variables ¶
var ( // ErrClosed ends calls and streams when the connection is gone. ErrClosed = errors.New("rpc: connection closed") // ErrStreamOverflow ends a stream whose reader fell StreamBuffer behind. ErrStreamOverflow = errors.New("rpc: stream reader too slow") )
var ErrBadErrorData = errors.New("rpc: malformed error data")
ErrBadErrorData: a registered type's wire detail did not decode.
var ( // ErrForbidden is what an allow func returns to refuse a call. ErrForbidden = errors.New("rpc: forbidden") )
Functions ¶
func DecodeDetail ¶
func DecodeDetail[V any](raw json.RawMessage) (*V, error)
DecodeDetail decodes raw into a *V strictly: a JSON object holding every field of struct V (by Go field name; nested values are not checked for completeness) and nothing else.
func MCPAllowed ¶
func MCPAllowed(method string, params []json.RawMessage) bool
MCPAllowed reports whether the mcp role may call method with params.
func RegisterType ¶
func RegisterType[T error](c *Codec, code string, encode func(T) any, decode func(json.RawMessage) (T, error))
RegisterType registers error type T under code. encode returns the safe fields to send (every string in them is redacted); decode rebuilds a T or fails, and Decode then returns ErrBadErrorData (see DecodeDetail).
func ServeConn ¶
func ServeConn(ctx context.Context, rwc net.Conn, d *Dispatcher, opts ConnOptions) error
ServeConn serves one connection until it closes, breaks a budget, or ctx ends. On return every stream has been cancelled and drained.
Types ¶
type Client ¶
type Client struct {
// DecodeError maps a wire error back to a Go error; the default returns
// the *WireError itself.
DecodeError func(*WireError) error
// contains filtered or unexported fields
}
Client is a JSON-RPC client. One read loop handles messages in order, so a stream is registered before any of its events are dispatched.
func Dial ¶
Dial connects to the Server in dataDir with the token it wrote and says hello as role. codec (nil = none) decodes error responses.
func (*Client) Call ¶
Call invokes method and decodes its result into result (nil discards it; a pointer to a slice receives multiple results). Cancelling ctx sends $/cancel and returns ctx.Err().
func (*Client) Stream ¶
func (c *Client) Stream(ctx context.Context, method string, args ...any) (events <-chan json.RawMessage, cancel func(), err error)
Stream invokes a stream method. events closes when the stream ends, the caller cancels, ctx ends, or the caller falls StreamBuffer events behind; StreamErr then says why. cancel is idempotent and releases the stream.
type Codec ¶
type Codec struct {
// contains filtered or unexported fields
}
Codec maps errors to stable wire codes and back. Only registered errors cross the wire with their own text; anything else is "internal error". Register before use; a Codec is then safe for concurrent use.
func (*Codec) Decode ¶
Decode maps a wire error back: a registered sentinel to the same value, a registered type to that type (wrapped so it also is its registered cause; an unknown cause is dropped), anything else to a *RemoteError.
type ConnOptions ¶
type ConnOptions struct {
MaxMessage int // largest inbound line, 16 MB
MaxActive int // in-flight requests plus live streams, 32
QueueMessages int // send queue, 1024 messages …
QueueBytes int // … or 32 MB; overflow drops the connection
WriteTimeout time.Duration // per write, 10 s
// Hello, if set, must authenticate the connection: the first request
// must be "hello"; its allow func then runs before every later call is
// dispatched. ErrForbidden refuses the call as forbidden; any other error
// stops it and is answered through EncodeError.
Hello func(params []json.RawMessage) (allow func(method string, params []json.RawMessage) error, result any, err error)
AuthTimeout time.Duration // 5 s
// EncodeError turns a target's error into a wire error; the default
// reveals nothing ("internal error").
EncodeError func(error) *WireError
// contains filtered or unexported fields
}
ConnOptions bounds one connection. Zero fields take the defaults.
type Dispatcher ¶
type Dispatcher struct {
// contains filtered or unexported fields
}
Dispatcher calls a target's exported methods by name.
func NewDispatcher ¶
func NewDispatcher(target any, expose func(method string) bool) (*Dispatcher, error)
NewDispatcher reflects target's exported methods; expose (nil = all) picks which ones are callable. An exposed method with an unsupported signature is an error, never silently skipped.
type RemoteError ¶
RemoteError is a wire error the Codec has no registration for.
func (*RemoteError) Error ¶
func (e *RemoteError) Error() string
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server serves a Dispatcher on <dataDir>/godwit.sock.
func Listen ¶
func Listen(dataDir string, d *Dispatcher, opts ServerOptions) (*Server, error)
Listen listens on <dataDir>/godwit.sock (0600 on Unix) and writes a fresh token to <dataDir>/api-token (0600). The caller must already hold the data directory's owner lock (package ownerlock): that is what makes an existing socket file a leftover, which Listen removes. Any other file there is an error and stays.
type ServerOptions ¶
type ServerOptions struct {
Conn ConnOptions // per connection; Hello and EncodeError are set by the Server
MaxConns int // simultaneous connections, 16
Version string // hello answers "godwit/<Version>"
// Codec encodes errors (nil: every error is "internal error"). Every
// string it sends, and every error logged, has the API token and then
// whatever Redact (nil = nothing more) finds replaced.
Codec *Codec
Redact func(string) string
// Audit, if set, runs before an authorized call is dispatched, with the
// connection's role. An error stops the call and is answered and logged
// like any error the call could have returned.
Audit func(role, method string, params []json.RawMessage) error
}
ServerOptions configures a Server. Zero fields take the defaults.
type WireError ¶
type WireError struct {
Code int `json:"code"`
Message string `json:"message"`
Data *WireErrorData `json:"data,omitempty"`
}
WireError is a JSON-RPC error object.
type WireErrorData ¶
type WireErrorData struct {
Code string `json:"code"`
Detail json.RawMessage `json:"detail,omitempty"`
Cause string `json:"cause,omitempty"`
}
WireErrorData carries a stable error code and safe fields. Cause is, for a registered type, the code of the first registered sentinel the error also is (errors.Is), so that survives the trip too.