Skip to main content

Go SDK

The Go SDK covers Rune's complete extension API. Install it into your module:

go get github.com/unstablebuild/rune-go-sdk

Requires Go 1.25 or newer.

A basic extension

The example below is the wiring half of the SDK's examples/snippets, a complete extension that adds a snippets command for storing and reusing text. main does only metadata and wiring; the logic lives in a handler typed against SDK interfaces so it stays testable.

package main

import (
"context"
"fmt"
"log/slog"
"os"

"github.com/unstablebuild/rune-go-sdk/api/config"
"github.com/unstablebuild/rune-go-sdk/api/extensionapi"
"github.com/unstablebuild/rune-go-sdk/api/textapi"
)

func main() {
meta := extensionapi.Metadata{
DeveloperID: "rune-sdk-examples",
DeveloperKey: "1234",
DeveloperEmail: "your@email.com",
ExtensionID: "snippets",
ExtensionName: "Snippets",
ExtensionVersion: "0.1.0",
Permissions: extensionapi.NewPermissions(
extensionapi.PermissionStorage,
extensionapi.PermissionEditor,
extensionapi.PermissionCommands,
extensionapi.PermissionFileSystem,
extensionapi.PermissionBrowserResourceOpener,
extensionapi.PermissionBrowserWindowManager,
extensionapi.PermissionNotifications,
),
}

if err := extensionapi.ServeWorkspaceExtension(
extensionapi.FuncWorkspaceExtension(run), meta,
); err != nil {
slog.Error("snippets exited", "error", err)
os.Exit(1)
}
}

// run wires capabilities, subscribes to editor events, and registers the
// command, then returns. ServeWorkspaceExtension owns the lifetime.
func run(ctx context.Context, ws *extensionapi.Workspace, cfg config.Config) error {
s := newSnippets(
ws.Storage(ctx), // persist snippets across sessions
ws.Editor(ctx), // insert bodies at the cursor
ws.FileSystem(ctx), // back scratch buffers with real files
ws.ResourceOpener(ctx), // open those buffers in a tab
ws.WindowManager(ctx), // focus the tab that was opened
ws.Notifications(ctx), // report success/failure to the user
)

events := []textapi.EventType{
textapi.EventTypeSelection,
textapi.EventTypeFlush,
textapi.EventTypeClose,
}
if err := ws.Editor(ctx).SubscribeEvents(events, s); err != nil {
return fmt.Errorf("subscribe events: %w", err)
}

manual := textapi.CommandManual{
Name: "snippets",
Summary: "Insert, edit, copy, or delete reusable text snippets.",
Synopsis: "[insert|edit|copy|delete] <name>",
}
if err := ws.RegisterCommand(manual, s); err != nil {
return fmt.Errorf("register command: %w", err)
}
return nil
}

ServeWorkspaceExtension writes the metadata to Rune, reads back the connection config, and serves until shutdown. Each Workspace accessor returns a typed client into the host:

PackageCapabilityAccessor
extensionapiHandshake, Workspace, command registration(none)
textapiEditor and text operations, editor events, commandsEditor, RegisterCommand
storageapiDocument storage and persistenceStorage
browserapiWindows, tabs, resource opening, notificationsWindowManager, ResourceOpener, Notifications
workspaceapiURI resolution, file operations, watchers, command executionFileSystem, Executor
semanticapiLanguage-server operations (definition, references, rename, diagnostics, and more)LSP
llmapiLLM access (models, token counting, messages)LLM
debugapiDebug-adapter controlDebugger
syntaxapiTree-sitter structural search and queriesParser
configMerged host configurationConfig

Every accessor is gated on the matching permission declared in Metadata.

Run and debug it

Build your binary, then start it in a workspace from the console:

extensions start snippets /path/to/snippets --config '{"key":"value"}'

To skip the build step during development, point extensions start at the module's main.go instead; Rune runs it with the go package's toolchain (see source-file extensions).

Use extensions logs snippets to read what the process wrote to stderr (write diagnostics with slog), and extensions restart snippets to pick up a rebuild. The full lifecycle and the permission prompts are covered in the development loop.

Test it

Keep main thin and put the logic in a handler typed against SDK interfaces, as in the example above, so it can be exercised against fakes in table-driven tests. For extensions that serve UIs, the SDK ships dedicated harnesses: comptest for components and handlertest for handlers. The examples/snippets extension shows the layout and the patterns worth copying.

Package it

A Go extension is distributed from a public git repository: no archive to build and no per-platform binaries, because Rune compiles it from source on the user's machine. Put the module at the repo root next to a config.yaml that requires the go package and points path at the module's main.go:

config.yaml
requirements:
- go
extensions:
snippets:
path: '$RUNE_DATADIR/lib/$RUNE_PKG_ID/main.go'
config:
# defaults surfaced in the user's config, editable like any other setting

Commit both go.mod and go.sum: Rune runs the module in read-only module mode, so the checked-out go.sum must already pin every dependency. Push it to any host Rune can clone over HTTPS (GitHub, GitLab, Bitbucket, or a self-hosted server) and users install it by its <host>/<owner>/<repo> ID:

pkg install github.com/<owner>/<repo>

Rune clones the repo, installs the go package (which provides the toolchain) first because the overlay requires it, and runs the module with go run (see source-file extensions). Tag a commit to give users a pinnable version. See the packages guide for the full format and how versions work.

Ask Rune Agent