Documentation
¶
Overview ¶
Package transport (github.com/opencharly/spec/transport, #55 step1) provides transport-neutral gRPC connections to Charly targets. This file holds the go-plugin serve/dispense surface relocated from the github.com/opencharly/sdk root package: the handshake, the Serve/PluginMap/Conn trio, and the channel streaming types an out-of-process plugin + the charly host share over the broker. It is a fabric slice of the spec contract module — a host primitive a plugin cannot hold as a shared library (the gRPC dial/serve over the go-plugin stdio carrier) — so by #55 Rule 1 it lives in a contained spec slice whose only heavy dep is google.golang.org/grpc + hashicorp/go-plugin + spec/proto (Rule 2). charly core imports this slice INSTEAD of the sdk root; the sdk root keeps a thin re-export during cutover then is deleted.
Package transport (github.com/opencharly/spec/transport, #55 step1) provides transport-neutral gRPC connections to Charly targets. It is a fabric slice of the spec contract module — a host primitive a plugin cannot hold as a shared library (the gRPC dial/serve over a process stdio carrier) — so by #55 Rule 1 it lives in a contained spec slice whose only heavy dep is google.golang.org/grpc + spec/proto (Rule 2). SSH is only a process transport: gRPC still owns the protocol, and the target chain carried by CUE-generated spec.TargetSpec owns nested routing.
Index ¶
- Constants
- Variables
- func CopyChannel(dst interface{ ... }, src interface{ ... }) error
- func DialProcessProvider(ctx context.Context, process spec.Process, opts DialOptions) (*grpc.ClientConn, pb.ProviderClient, error)
- func DialProvider(ctx context.Context, target spec.TargetSpec, opts DialOptions) (*grpc.ClientConn, pb.ProviderClient, error)
- func IsServeMode() bool
- func Main(providerSrv pb.ProviderServer, metaSrv pb.PluginMetaServer, ...)
- func OpenProviderChannel(ctx context.Context, client pb.ProviderClient, open *pb.ChannelFrame) (pb.Provider_ChannelClient, error)
- func PluginMap(providerSrv pb.ProviderServer, metaSrv pb.PluginMetaServer) plugin.PluginSet
- func ReceiveChannelOpen(stream ProviderChannel) (*pb.ChannelFrame, error)
- func RelayChannel(upstream ProviderChannel, downstream interface{ ... }) error
- func Serve(providerSrv pb.ProviderServer, metaSrv pb.PluginMetaServer)
- func ServeStdio(stdin io.ReadCloser, stdout io.WriteCloser, register func(*grpc.Server), ...) error
- func StdioCommand(target spec.TargetSpec, opts DialOptions) ([]string, error)
- func StdioFiles() (io.ReadCloser, io.WriteCloser)
- type ChannelProvider
- type Conn
- type DialOptions
- type ProviderChannel
- type ReplayBuffer
- type SequenceGate
Constants ¶
const ( ChannelOpen = "open" ChannelStdin = "stdin" ChannelStdout = "stdout" ChannelStderr = "stderr" ChannelTerminal = "terminal" ChannelStatus = "status" ChannelResize = "resize" ChannelSignal = "signal" ChannelAck = "ack" ChannelCancel = "cancel" ChannelExit = "exit" ChannelError = "error" ChannelResync = "resync" )
Channel frame kinds are transport vocabulary, not runtime semantics. Runtime- specific events travel as CUE-generated JSON in ChannelFrame.PayloadJson.
const DispenseKey = "charly"
DispenseKey is the single go-plugin plugin name; charly serves/dispenses ONE gRPC plugin exposing the uniform Provider + PluginMeta services.
Variables ¶
var Handshake = plugin.HandshakeConfig{
ProtocolVersion: 2,
MagicCookieKey: "CHARLY_PLUGIN",
MagicCookieValue: "charly-plugin-v1",
}
Handshake is the magic-cookie handshake charly and every plugin MUST share. A plugin server refuses to serve unless launched with CHARLY_PLUGIN set, so a plugin binary run by hand prints the "not meant to be executed directly" notice instead of hanging.
ProtocolVersion is hashicorp/go-plugin's OWN transport-family negotiation: its checkProtoVersion runs at Connect, before Describe. The value is FROZEN at 2 and MUST NOT be changed without rebuilding every plugin binary — go-plugin refuses a plugin whose advertised version differs, and that would break plugins whose WIREFRAME is perfectly compatible. It is NOT the wireframe gate: the wireframe contract is the CUE schema a plugin serves over Describe and the host splices onto its base (registerPluginUnitSchema), where a real capability gap is a hard schema error.
var ServedBroker *plugin.GRPCBroker
ServedBroker is the go-plugin GRPCBroker captured when this plugin's gRPC server starts (grpcPlugin.GRPCServer). A deploy/step/builder plugin dials the host's E3b reverse-channel ExecutorService through it. One broker per plugin process (go-plugin's model), so a package var is the natural home. Exported so the sdk package's executor/checkverb helpers can read it after relocation.
Functions ¶
func CopyChannel ¶ added in v0.2026213.2047
func CopyChannel(dst interface{ Send(*pb.ChannelFrame) error }, src interface {
Recv() (*pb.ChannelFrame, error)
}) error
CopyChannel relays frames until EOF or cancellation. It is intentionally a byte-preserving transport primitive; it does not inspect agent or terminal payloads.
func DialProcessProvider ¶
func DialProcessProvider(ctx context.Context, process spec.Process, opts DialOptions) (*grpc.ClientConn, pb.ProviderClient, error)
DialProcessProvider opens gRPC over a process supplied by a deployment executor. This is the placement-neutral sibling of DialProvider: the same protocol runs over local exec, SSH, containers, VMs, and recursively nested venues without teaching the gRPC layer about any of them.
func DialProvider ¶
func DialProvider(ctx context.Context, target spec.TargetSpec, opts DialOptions) (*grpc.ClientConn, pb.ProviderClient, error)
DialProvider opens gRPC over a local-exec or SSH stdio process. The SSH remote argv is fixed to `charly __agent-target serve --stdio`; nested target hops are carried as TargetSpec on the first Provider.Channel frame and are resolved by the responsible Charly node.
func IsServeMode ¶ added in v0.2026213.2047
func IsServeMode() bool
IsServeMode reports whether this process was launched by charly as a go-plugin gRPC SERVER (the handshake magic-cookie env is present) rather than invoked directly as a CLI. charly sets the cookie ONLY when it execs a plugin to connect over gRPC (LocalTransport, for a verb/kind/deploy/step/builder capability); a COMMAND plugin fork/exec'd as a CLI passthrough (charly's syscall.Exec command dispatch strips the cookie) sees it absent and runs in CLI mode. The single switch a dual-mode plugin's main() pivots on.
func Main ¶ added in v0.2026213.2047
func Main(providerSrv pb.ProviderServer, metaSrv pb.PluginMetaServer, cli func(args []string) int)
Main is the dual-mode entry point a plugin's main() delegates to. In SERVE mode (charly launched it over go-plugin gRPC) it serves the plugin's Provider + PluginMeta (its verb/kind/deploy/step/builder capabilities). Otherwise the plugin was fork/exec'd by charly's COMMAND dispatch (or run by hand) and owns real terminal stdio/TTY: cli runs the command's work with os.Args[1:], its int return becoming the process exit code.
func main() { transport.Main(&provider{}, &meta{}, cliMain) }
func OpenProviderChannel ¶ added in v0.2026213.2047
func OpenProviderChannel(ctx context.Context, client pb.ProviderClient, open *pb.ChannelFrame) (pb.Provider_ChannelClient, error)
OpenProviderChannel starts a generated Provider.Channel stream and sends its mandatory open frame. The returned stream is ready for concurrent Send/Recv, as supported by gRPC.
func PluginMap ¶ added in v0.2026213.2047
func PluginMap(providerSrv pb.ProviderServer, metaSrv pb.PluginMetaServer) plugin.PluginSet
PluginMap builds the go-plugin PluginSet for the dispense key. Server side passes the two service impls; the client side (charly connecting) passes nil,nil and receives a *Conn from the dispense.
func ReceiveChannelOpen ¶ added in v0.2026213.2047
func ReceiveChannelOpen(stream ProviderChannel) (*pb.ChannelFrame, error)
ReceiveChannelOpen reads and validates the mandatory first frame. The request id, provider class/word, and operation are required so every later frame can be correlated without inspecting runtime-specific payloads.
func RelayChannel ¶ added in v0.2026213.2047
func RelayChannel(upstream ProviderChannel, downstream interface { ProviderChannel CloseSend() error }) error
RelayChannel connects a controller-side ProviderChannel to a downstream gRPC channel with ordered half-close semantics. Provider output is the evidence writer: once that direction ends, no later frame can mutate controller state and the relay may return. If controller input ends first, CloseSend delivers the protocol EOF and the relay drains provider output before returning. This prevents a completed command from racing its successor's durable cursor.
Cancellation ownership: when the provider-output direction finishes first, RelayChannel returns while the input-copy goroutine may still be blocked in upstream.Recv(). The CALLER owns upstream's lifecycle — after return, cancel the context upstream carries (or close upstream) to release that goroutine.
func Serve ¶ added in v0.2026213.2047
func Serve(providerSrv pb.ProviderServer, metaSrv pb.PluginMetaServer)
Serve exposes a plugin's Provider + PluginMeta services over go-plugin gRPC and blocks serving. The host reaps it on exit by killing the client connection (providerRegistry.Close → client.Kill, sending the gRPC Shutdown that stops this server); go-plugin's server has no parent-death detection of its own, so watchParentDeath is the backstop that self-terminates this process if the host dies without reaping (crash / SIGKILL / os.Exit) — preventing orphaned `__plugin serve` processes. The serve half of Main (a verb/kind/deploy/step/ builder plugin with no CLI mode may call it directly):
func main() { transport.Serve(&myProvider{}, &myMeta{}) }
func ServeStdio ¶
func ServeStdio(stdin io.ReadCloser, stdout io.WriteCloser, register func(*grpc.Server), opts ...grpc.ServerOption) error
ServeStdio runs a gRPC server on one full-duplex stdio connection. stdout is exclusively HTTP/2 protocol data; diagnostics belong on stderr.
func StdioCommand ¶
func StdioCommand(target spec.TargetSpec, opts DialOptions) ([]string, error)
StdioCommand validates the generic route and returns an argv with no shell interpolation. Hops after the first grpc marker—including tmux, another SSH node, or an exec/container hop—are nested routing data, not special cases.
func StdioFiles ¶
func StdioFiles() (io.ReadCloser, io.WriteCloser)
StdioFiles is the production adapter used by the hidden target command.
Types ¶
type ChannelProvider ¶ added in v0.2026213.2047
type ChannelProvider interface {
OpenChannel(open *pb.ChannelFrame, stream ProviderChannel) error
}
ChannelProvider is the optional streaming extension to Provider. The first frame has already been validated as an open frame and remains available as open; subsequent controller frames arrive through stream. Domain payloads are generated from CUE and carried in open.PayloadJson.
type Conn ¶ added in v0.2026213.2047
type Conn struct {
Provider pb.ProviderClient
Meta pb.PluginMetaClient
// Broker is this connection's go-plugin GRPCBroker — the host's handle to stand up
// the E3b reverse-channel ExecutorService a deploy/step/builder plugin dials back
// to. Nil for an in-proc transport (no reverse channel needed).
Broker *plugin.GRPCBroker
}
Conn is the dispensed client handle — charly's side of a connected plugin.
type DialOptions ¶
type DialOptions struct {
CharlyBinary string
RemoteCharlyBinary string
SSHBinary string
Stderr io.Writer
ExtraGRPC []grpc.DialOption
Command func(context.Context, string, ...string) *exec.Cmd
}
DialOptions controls process launch only. It deliberately has no runtime- or tmux-specific fields; those remain in the CUE payload sent over Provider.Channel.
type ProviderChannel ¶ added in v0.2026213.2047
type ProviderChannel interface {
Context() context.Context
Send(*pb.ChannelFrame) error
Recv() (*pb.ChannelFrame, error)
}
ProviderChannel is the common subset of the generated client and server streams. It lets in-process and gRPC providers share one channel handler.
type ReplayBuffer ¶ added in v0.2026213.2047
type ReplayBuffer struct {
// contains filtered or unexported fields
}
ReplayBuffer is a bounded, acknowledgement-aware frame history for detach / reconnect. Bounds are enforced by both frame count and protobuf byte size. When an unacknowledged frame would be evicted, Add fails loudly; callers must preserve evidence and enter the incident/RCA workflow rather than hide loss.
func NewReplayBuffer ¶ added in v0.2026213.2047
func NewReplayBuffer(maxFrames, maxBytes int) *ReplayBuffer
func (*ReplayBuffer) Acknowledge ¶ added in v0.2026213.2047
func (b *ReplayBuffer) Acknowledge(sequence uint64)
func (*ReplayBuffer) Add ¶ added in v0.2026213.2047
func (b *ReplayBuffer) Add(frame *pb.ChannelFrame) error
func (*ReplayBuffer) ReplayFrom ¶ added in v0.2026213.2047
func (b *ReplayBuffer) ReplayFrom(sequence uint64) ([]*pb.ChannelFrame, error)
type SequenceGate ¶ added in v0.2026213.2047
type SequenceGate struct {
// contains filtered or unexported fields
}
SequenceGate rejects duplicates, regressions, and gaps. A provider can turn a gap into ChannelResync using ReplayBuffer.ReplayFrom; it must never silently reorder process or terminal output.
func NewSequenceGate ¶ added in v0.2026213.2047
func NewSequenceGate(first uint64) *SequenceGate
func (*SequenceGate) Accept ¶ added in v0.2026213.2047
func (g *SequenceGate) Accept(frame *pb.ChannelFrame) error
func (*SequenceGate) Expected ¶ added in v0.2026213.2047
func (g *SequenceGate) Expected() uint64
Expected returns the next sequence without advancing the gate.