Skip to Content
Numinance
Integrations

Write a plugin in Go

Build a capability provider with the Go plugin SDK, using the sample knowledge provider as a line-by-line reference, and prove it against the conformance kit.

View as .mdUpdated
Pre-alpha

The Go plugin SDK and the sample provider are in the public tree and build today. The sample is a teaching and conformance target, not a product connector. There is no plugin host in the open tree, so a provider you write cannot be launched end to end by open code alone.

A Go provider is a main that calls plugin.Serve, a PluginService that returns an honest manifest and descriptors, and a dispatch that decodes the envelope and answers with one typed result. Build once against the contract.

When to use this

Use this page when you want the exact shape of a working provider before writing your own, or when you need to know what the SDK does for you and what it leaves to you. It supersedes the older adapter development outline; the guide at Build a custom plugin is the task-oriented version, and First plugin walks the first build.

Prerequisites

  • A clone of numinance-spec and the Go toolchain go.mod asks for (go 1.27, toolchain go1.27.3). See Install.
  • Familiarity with gRPC server-streaming in Go.
  • The port you implement. Read its .proto header comment first: it states who may call it, the authority model, and the bounds.

Imports and module path

The module path is github.com/numinance/numinance-spec. The repository is cloned from github.com/dsha256/numinance-spec, but the Go module name inside go.mod is the one you import. The sample uses the following imports.

plugins/examples/sample-knowledge/main.go (imports)
import (
	"context"
	"fmt"
	"os"
	"os/signal"

	"google.golang.org/grpc"

	"github.com/numinance/numinance-spec/plugins/examples/sample-knowledge/provider"
	"github.com/numinance/numinance-spec/sdk/go/plugin"
)
plugins/examples/sample-knowledge/provider/server.go (imports)
import (
	annotationsv1 "github.com/numinance/numinance-spec/sdk/go/gen/numinance/annotations/v1"
	pluginv1 "github.com/numinance/numinance-spec/sdk/go/gen/numinance/plugin/v1"
	knowledgev1 "github.com/numinance/numinance-spec/sdk/go/gen/numinance/ports/knowledge/v1"
	typesv1 "github.com/numinance/numinance-spec/sdk/go/gen/numinance/types/v1"
	"github.com/numinance/numinance-spec/sdk/go/plugin"
)

sdk/go/plugin is the handwritten runtime support (handshake, Serve, DescriptorSet, StreamCheck). The sdk/go/gen/... packages are the generated stubs of the protocol files.

The entry point

main.go is the whole process wiring. This is the complete file.

plugins/examples/sample-knowledge/main.go
// Command sample-knowledge is the sample knowledge provider: a capability provider of numinance.ports.knowledge.v1 over a
// small in-memory source. A plugin host launches it as a subprocess; run by hand it refuses and says so.
package main

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

	"google.golang.org/grpc"

	"github.com/numinance/numinance-spec/plugins/examples/sample-knowledge/provider"
	"github.com/numinance/numinance-spec/sdk/go/plugin"
)

func main() {
	if err := run(); err != nil {
		fmt.Fprintln(os.Stderr, "sample-knowledge:", err)
		os.Exit(1)
	}
}

func run() error {
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
	defer stop()
	digest, err := provider.ExecutableDigest()
	if err != nil {
		return err
	}
	return plugin.Serve(ctx, os.Getenv, os.Stdout, func(s *grpc.Server, rt plugin.Runtime) {
		provider.New(digest, rt.Stop).Register(s)
	})
}

What plugin.Serve does for you:

  1. Checks NUMINANCE_PLUGIN_COOKIE against plugin.MagicCookie and returns plugin.ErrNotLaunchedByHost otherwise.
  2. Parses the host's protocol offer and picks AppProtocolVersion if it is in the offer.
  3. Listens on a Unix socket inside a new 0700 directory and removes it on return.
  4. Calls your register function with the gRPC server and a plugin.Runtime.
  5. Prints the handshake line and serves until the context ends or Runtime.Stop is called.

What Runtime offers

sdk/go/plugin/serve.go
type Runtime struct {
	// HostAddr is the gRPC target of the host's HostService, "" when the host passed none.
	HostAddr string
	// Stop ends Serve; PluginService.Shutdown calls it after it answers.
	Stop func()
}
  • Stop is how your Shutdown handler ends the process after it has answered. The sample calls it in a goroutine so the reply is sent first.
  • HostAddr is how you reach HostService. The sample is read-only and ignores it. A provider that needs a credential handle or emits evidence dials it: the official GitHub providers call ghprovider.DialHost(rt.HostAddr) and, if that fails, call rt.Stop().

The PluginService

The provider type embeds the generated UnimplementedPluginServiceServer and implements the six lifecycle RPCs. It is shown in full; the numbered comments below the listing explain each choice.

  • New builds the manifest once. Kind is EXTENSION_CLASS_CAPABILITY_PROVIDER, Ports names numinance.ports.knowledge.v1, and each of the three verbs gets a CapabilityDescriptor with RiskFloor READ, SideEffect READ and Idempotency NATURAL, because a read-only verb may not claim more. ConformanceSuite names the kit the host will look up.
  • ExecutableDigest hashes the running binary so the declared digest is real. main passes it in.
  • Handshake accepts only a protocol version equal to plugin.AppProtocolVersion and answers with the spec version the sample was built against. It declares no non-Core capability.
  • GetDescriptors returns plugin.DescriptorSet(knowledgev1.File_numinance_ports_knowledge_v1_knowledge_proto): the generated port file and everything it imports, source info dropped. Do not edit these.
  • Invoke refuses a call that has no host-assigned invocation_id or policy_decision_id, dispatches by Envelope.Method, and sends exactly one terminal message: SUCCEEDED with the packed result, or FAILED with one Diagnostic whose Reason comes from the closed vocabulary. There is no free text, so no provider output can inject instructions into a log.
  • dispatch checks the request's type URL equals type.googleapis.com/ plus the method's input type before unmarshalling, and answers an unknown method with Unimplemented.

The source

The provider calls a small in-memory Source. The code shows how a provider validates inputs against the port's bounds and never repairs them.

Notes on what it models, because these are the habits a real provider needs:

  • Every request is validated against the port grammar (query length, scope entries, max_results between 1 and 100) before the source is touched. A violation returns INVALID_ARGUMENT.
  • Scope entries are compared as whole segments, never as raw string prefixes, and are never decoded or normalised, so kit/%2E%2E/other fails the grammar.
  • Returned text is cleaned: control, format and bidi characters become U+FFFD, and truncation happens at a character boundary.
  • A document outside the requested scope is NOT_FOUND, never PERMISSION_DENIED, so existence is not leaked.

Build it

These commands were run against the pinned checkout in the documentation environment. The offline module cache and a local toolchain were needed there; with network access a plain go build is enough.

Environment used for the verified run
export GOTOOLCHAIN=local GOCACHE=$TMPDIR/gocache GOMODCACHE=$HOME/go/pkg/mod GOPROXY=off GOFLAGS=-mod=mod
go build -o "$TMPDIR/sample-knowledge" ./plugins/examples/sample-knowledge
(no output; exit 0)
✓ verifiedgo build of the sample (Go 1.27.2 local toolchain)· numinance-spec @ 3c74d59 · 2026-11-09· 0

The only message printed was a Go cache warning about writing a stat-cache entry that the sandbox denied; it did not affect the result.

Run it by hand to see the guard:

sample-knowledge: this binary is a Numinance provider and is started by the plugin host
✓ verifiedthe built binary started from a shell· numinance-spec @ 3c74d59 · 2026-11-09· 1

go vet ./plugins/examples/... exited 0. go test ./plugins/examples/... printed [no test files] for both packages: the sample has no tests of its own. Its tests are the conformance kit.

Test it against the conformance kit

conformance/knowledge/v1 is a Go package, knowledgekit, that runs a fixed list of cases against anything implementing a three-method Target interface.

conformance/knowledge/v1/kit.go
type Target interface {
	Search(context.Context, *knowledgev1.SearchRequest) (*knowledgev1.SearchResponse, error)
	Fetch(context.Context, *knowledgev1.FetchRequest) (*knowledgev1.FetchResponse, error)
	Resolve(context.Context, *knowledgev1.ResolveRequest) (*knowledgev1.ResolveResponse, error)
}

The kit judges only what comes back. How you reach the provider is up to you: a real host dispatching Invoke, a replay, or a direct call. The kit's own test calls the sample's Source directly with a thin adapter that translates the provider's CallError into the kit's, and runs it with knowledgekit.Run(t, direct{provider.NewSource()}). A second test feeds the kit three deliberately broken providers (one that ignores scope, one that accepts kit/../other, one that splices query text) and asserts each fails at least one case.

Run the knowledge kit against the sample
go test -count=1 -v ./conformance/knowledge/...
--- PASS: TestKit_TheSampleProviderPassesTheSkeleton (0.00s)
    --- PASS: TestKit_TheSampleProviderPassesTheSkeleton/CORE-001 (0.00s)
    --- PASS: TestKit_TheSampleProviderPassesTheSkeleton/CORE-002 (0.00s)
    --- PASS: TestKit_TheSampleProviderPassesTheSkeleton/CORE-003 (0.00s)
    --- PASS: TestKit_TheSampleProviderPassesTheSkeleton/CORE-004 (0.00s)
    --- PASS: TestKit_TheSampleProviderPassesTheSkeleton/CORE-005 (0.00s)
    --- PASS: TestKit_TheSampleProviderPassesTheSkeleton/CORE-007 (0.00s)
    --- PASS: TestKit_TheSampleProviderPassesTheSkeleton/CORE-008 (0.00s)
    --- PASS: TestKit_TheSampleProviderPassesTheSkeleton/CORE-012 (0.00s)
    --- PASS: TestKit_TheSampleProviderPassesTheSkeleton/CORE-013 (0.00s)
    --- PASS: TestKit_TheSampleProviderPassesTheSkeleton/CORE-014 (0.00s)
    --- PASS: TestKit_TheSampleProviderPassesTheSkeleton/CORE-015 (0.00s)
--- PASS: TestKit_FailsABrokenProvider (0.00s)
--- PASS: TestKit_CasesAreListedInTheREADME (0.00s)
PASS
ok  	github.com/numinance/numinance-spec/conformance/knowledge/v1	0.186s
✓ verifiedknowledge kit against the sample provider (excerpt)· numinance-spec @ 3c74d59 · 2026-11-09· 0

What this does not prove. The kit runs eleven of the cases in the README table against a seeded source. The remaining cases (principals, a one-megabyte document, a broken or unreachable source, a rate-limited source) need fixtures the skeleton does not carry. The host checks (scope refusal before Invoke, digest and bounds checks, unknown-field rejection, egress against metadata and private addresses) are enforced by the plugin host, which is not in the open tree. A pass here is evidence that your verb logic matches the port's core cases; it is not a pass of the full port and not a security review.

Write your own

Choose the port and read its header

The header of proto/numinance/ports/<port>/v1/<port>.proto states callers, authority and bounds. Take the generated stub from sdk/go/gen/numinance/ports/<port>/v1.

Declare honestly

For each verb you implement, add a CapabilityDescriptor at or above the port's risk and side-effect floors. Declare every credential, network and filesystem intent you need and nothing else. If a verb is above READ, declare the evidence it emits. See Manifest and capabilities.

Serve with the SDK

Call plugin.Serve with your register function. Return the generated descriptor via plugin.DescriptorSet. Dispatch on Envelope.Method, check the request type URL, and answer with exactly one terminal message on a unary-shaped verb.

Never hold credentials

Ask HostService.RequestCredential for a handle and send requests through the egress proxy the response names. Do not read tokens from the environment: your environment is the three handshake variables.

Build a Target and run the kit

Write a test that adapts your provider to the port's Target and calls the kit's Run. Add your own tests for what the kit does not cover.

Failure modes

SymptomCause
Serve returns ErrNotLaunchedByHostStarted without the cookie. Expected outside a host
the host serves no protocol version this provider speaksNUMINANCE_PLUGIN_PROTOCOLS does not include plugin.AppProtocolVersion
Kit case fails with a reason mismatchA bound violation returned the wrong Reason (for example NOT_FOUND for an invalid argument)
Host would reject a streamA RUNNING message carried a result on a unary target, or a terminal message carried none on success
Install would refuse the packagePlaceholder SBOM and provenance references, as in the sample

Next steps

On this page