> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ubiquex.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Writing a Plugin

> Implement a custom ubx engine plugin using the gRPC protocol.

A ubx plugin is any executable binary that implements the `ubx.plugin.v1.PluginService` gRPC service. You can write plugins in any language with gRPC support. Core and plugins share only the protobuf contract — there is no shared SDK.

## Service

The full service and message definitions are in the [Plugin Protocol Reference](/v1/plugins/protocol). A plugin must implement all six RPCs:

```protobuf theme={"theme":"css-variables","languages":{"custom":["/languages/xcl.json"]}}
service PluginService {
  rpc Handshake (HandshakeRequest) returns (HandshakeResponse);
  rpc Schema    (SchemaRequest)    returns (SchemaResponse);
  rpc Compile   (CompileRequest)   returns (CompileResponse);
  rpc Plan      (PlanRequest)      returns (PlanResponse);
  rpc Apply     (ApplyRequest)     returns (stream ProgressEvent);
  rpc Destroy   (DestroyRequest)   returns (stream ProgressEvent);
}
```

`Schema` may return an empty `SchemaResponse` if the plugin does not enumerate its types — core then skips type-level validation for it. The official plugins do exactly this.

## Launch handshake

Core launches the plugin binary with the environment variable `UBX_PLUGIN_LAUNCH=1`. The plugin must:

1. Refuse to run unless `UBX_PLUGIN_LAUNCH=1` is set (a magic cookie preventing accidental direct execution).
2. Bind a random loopback TCP port (`127.0.0.1:0`).
3. Print `UBX_PLUGIN_ADDR=127.0.0.1:<port>` to stdout before serving.
4. Serve until it receives `SIGINT`/`SIGTERM` from core.

Core then dials that address over insecure gRPC (loopback only) and calls `Handshake` first, sending protocol version `"1.0"`. Return a non-empty `HandshakeResponse.error` if you cannot speak that version. Core sends `SIGINT` when the invocation finishes and `SIGKILL` after 5 seconds if the process has not exited.

## CompileRequest

```protobuf theme={"theme":"css-variables","languages":{"custom":["/languages/xcl.json"]}}
message CompileRequest {
  string    stack_dir         = 1;   // absolute path to the stack source directory
  StackMeta meta              = 2;   // name, backend_uri, config map
  bytes     ir_json           = 3;   // JSON-serialised IRProgram
  string    ir_schema_version = 4;   // current value: "1"
}

message CompileResponse {
  string          artifact_uri = 1;   // opaque URI passed back to Plan/Apply/Destroy
  repeated string warnings     = 2;
  string          error        = 3;   // non-empty if compilation failed
}
```

`Plan`, `Apply`, and `Destroy` receive only `artifact_uri` and `meta` — the artifact URI you returned from `Compile`. `Apply` and `Destroy` stream `ProgressEvent` and must end with exactly one `SummaryEvent`. See the [protocol reference](/v1/plugins/protocol) for the full message set.

## IRProgram structure

The `ir_json` field is a JSON-serialised `IRProgram`. Unmarshal it into your own types:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/xcl.json"]}}
{
  "stack_name":   "networking",
  "engine":       "myengine",
  "runtime":      "",
  "inputs":       [],
  "locals":       [],
  "resources":    [],
  "outputs":      [],
  "providers":    [],
  "backend":      {}
}
```

Resource declarations include `type_path` (array of segments) and `props` (typed IR expressions). Check `ir_schema_version` and return `CompileResponse.error` if you do not recognise it.

## Minimal Go plugin skeleton

```go theme={"theme":"css-variables","languages":{"custom":["/languages/xcl.json"]}}
package main

import (
    "context"
    "fmt"
    "net"
    "os"
    "os/signal"
    "syscall"

    pluginv1 "github.com/ubiquex/ubx-plugin-protocol/pluginv1"
    "google.golang.org/grpc"
)

type server struct {
    pluginv1.UnimplementedPluginServiceServer
}

func (s *server) Handshake(_ context.Context, req *pluginv1.HandshakeRequest) (*pluginv1.HandshakeResponse, error) {
    if req.CoreProtocolVersion != "1.0" {
        return &pluginv1.HandshakeResponse{
            Error: fmt.Sprintf("unsupported protocol version %q — plugin supports 1.0", req.CoreProtocolVersion),
        }, nil
    }
    return &pluginv1.HandshakeResponse{
        PluginProtocolVersion: "1.0",
        PluginName:            "ubx-plugin-myengine",
    }, nil
}

func (s *server) Compile(_ context.Context, req *pluginv1.CompileRequest) (*pluginv1.CompileResponse, error) {
    // Parse req.IrJson, generate an artifact, return its URI.
    return &pluginv1.CompileResponse{ArtifactUri: "file:///tmp/myengine-artifact"}, nil
}

// ... implement Schema, Plan, Apply, Destroy

func main() {
    if os.Getenv("UBX_PLUGIN_LAUNCH") != "1" {
        fmt.Fprintln(os.Stderr, "start via ubx, not directly")
        os.Exit(1)
    }

    lis, err := net.Listen("tcp", "127.0.0.1:0")
    if err != nil {
        panic(err)
    }

    srv := grpc.NewServer()
    pluginv1.RegisterPluginServiceServer(srv, &server{})

    // Tell core where to dial — before serving.
    fmt.Printf("UBX_PLUGIN_ADDR=%s\n", lis.Addr())

    go srv.Serve(lis)

    ch := make(chan os.Signal, 1)
    signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
    <-ch
    srv.GracefulStop()
}
```

## Installing a custom plugin

```bash theme={"theme":"css-variables","languages":{"custom":["/languages/xcl.json"]}}
ubx plugin install file://./path/to/ubx-plugin-myengine
```

This copies the binary to `~/.ubx/plugins/<name>/ubx-plugin-<name>` and records it in `.ubx/plugin.lock`.

## Naming convention

Plugin binaries follow the `ubx-plugin-<name>` convention. The `<name>` part is the engine name used in `workspace.xcl`.

<Note>
  Core's typechecker currently accepts `native`, `pulumi`, `terraform`, and `opentofu` as engine names. A custom engine name will be rejected at type-check time until it is added to core's valid-engine set.
</Note>
