rpc

package
v1.0.1 Latest Latest
Warning

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

Go to latest
Published: Oct 14, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

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

View Source
const (
	CodeParse           = -32700
	CodeInvalidRequest  = -32600
	CodeMethodNotFound  = -32601
	CodeInvalidParams   = -32602
	CodeInternal        = -32603
	CodeTooMany         = -32000
	CodeForbidden       = -32001
	CodeUnauthenticated = -32002
)

Error codes.

View Source
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.

View Source
const (
	SocketName = "godwit.sock"
	TokenName  = "api-token"
)

File names under the data directory.

View Source
const CodeApp = -32003

CodeApp is the wire code of a registered (application) error.

View Source
const StreamBuffer = 256

StreamBuffer is how many unread events a client stream holds before the client cancels it.

Variables

View Source
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")
)
View Source
var ErrBadErrorData = errors.New("rpc: malformed error data")

ErrBadErrorData: a registered type's wire detail did not decode.

View Source
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

func Dial(dataDir, role, client string, codec *Codec) (*Client, error)

Dial connects to the Server in dataDir with the token it wrote and says hello as role. codec (nil = none) decodes error responses.

func NewClient

func NewClient(conn io.ReadWriteCloser) *Client

NewClient starts reading conn.

func (*Client) Call

func (c *Client) Call(ctx context.Context, method string, result any, args ...any) error

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) Close

func (c *Client) Close() error

Close closes the connection; pending calls and streams end with ErrClosed.

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.

func (*Client) StreamErr

func (c *Client) StreamErr(events <-chan json.RawMessage) error

StreamErr reports why events closed: nil for a normal end or a cancel by the caller. Ask before calling cancel, which forgets it.

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

func (c *Codec) Decode(we *WireError) error

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.

func (*Codec) Encode

func (c *Codec) Encode(err error, redact func(string) string) *WireError

Encode turns err into a wire error: registered types first (errors.As, in registration order; the first sentinel err also is goes along as Cause), then sentinels (errors.Is); redact (nil = none) cleans every string sent for a type.

func (*Codec) RegisterSentinel

func (c *Codec) RegisterSentinel(code string, err error, message string)

RegisterSentinel registers err under code; it crosses as the fixed message and decodes to err itself.

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

type RemoteError struct {
	Code    int
	Message string
}

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.

func (*Server) Close

func (s *Server) Close() error

Close stops accepting, drops every connection, waits for their streams to drain, and removes the socket and token files. Every call, concurrent ones included, returns once that has finished, with the same result.

func (*Server) Path

func (s *Server) Path() string

Path is the socket's path.

func (*Server) Serve

func (s *Server) Serve(ctx context.Context) error

Serve accepts connections until Close or until ctx ends, which closes the Server. It returns nil once closed and Close's cleanup has finished.

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.

func (*WireError) Error

func (e *WireError) Error() string

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.

Jump to

Keyboard shortcuts

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