Go Modules
A Go module compiles to a single static binary that the daemon launches by path. The
SDK is dependency-light and lives at sdk/go/hovel
(github.com/vibepwners/hovel/sdk/go/hovel); the worked examples are
under modules/examples/go/. The shapes mirror the
Python SDK one-to-one.
Generated references are published for
hovel and
hoveltest.
The Module interface
type Module interface {
Info() Info // → handshake
Schema() Schema // → schema
Run(ctx *Context) (Result, error) // → execute
}
func main() { hovel.Serve(MyModule{}) }
Serve runs the JSON-RPC loop on stdin/stdout. Info and
Schema must be cheap and side-effect free — the daemon calls them while
building its catalog.
Do not write to stdout yourself. The SDK serializes responses and
notifications there; banners, fmt.Println, inherited subprocess
stdout, or progress bars will corrupt the framed stream. Use ctx.Log,
artifacts, or sessions for operator-visible output. Stderr is reserved for
process-level diagnostics that help explain startup or crash failures.
TLS, workspace PKI, and Mesh bridges
An ordinary Go module can use crypto/tls inside Run
without implementing any Hovel credential interface. Add the credential-provider
contracts only when the module itself consumes or transforms assignment-bound
workspace-PKI material. Hovel does not automatically inject a PKI bundle into a
normal non-Mesh Run call.
| Provider need | Go surface |
|---|---|
| Describe strict slots and capabilities | CredentialDescriber.DescribeCredentialDelivery |
| Consume in-memory bytes or a scoped reference | CredentialRuntimeProvider.LoadRuntimeCredential |
| Consume invocation-scoped protected files | CredentialFilesProvider.LoadCredentialFiles |
| Produce one bounded provider-native encoding | CredentialEncodingProvider.EncodeCredentialMaterial |
| Stamp an artifact or provider-owned deployment | CredentialStampProvider.StampCredential |
var (
_ hovel.CredentialDescriber = (*TLSProvider)(nil)
_ hovel.CredentialRuntimeProvider = (*TLSProvider)(nil)
)
Implement only the interfaces advertised by the descriptor. If a Mesh
descriptor also embeds CredentialDelivery, it must match the
standalone descriptor exactly. The daemon can currently initiate direct runtime
certificate DER, public-key SPKI, and private-key PKCS #8 delivery immediately
before a Mesh listener, task, stream, or bridge in the same process. General
non-Mesh delivery and external encoding/stamping initiation remain extension
points. See TLS and Workspace PKI,
Credential Provider Development,
and the generated Go SDK API.
Dial an authenticated Mesh bridge
A consumer that already speaks TCP or UDP needs no Mesh-provider interface.
Receive localHost, localPort,
localNetwork, and the ephemeral capability from
OpenMeshBridge, then let the SDK consume the authentication
preface before protocol bytes are sent. Use the returned local network;
do not infer it from the provider-defined request.protocol.
func dialBridge(
ctx context.Context,
host string,
port int,
network hovel.MeshBridgeNetwork,
capability string,
) (net.Conn, error) {
endpoint, err := hovel.NewMeshBridgeEndpoint(
host,
port,
network,
capability,
)
if err != nil {
return nil, err
}
return hovel.DialMeshBridge(ctx, endpoint)
}
Convert the response's localNetwork to
hovel.MeshBridgeNetwork. The validated endpoint retains it, so
callers cannot accidentally dial a UDP bridge as TCP or vice versa;
construction rejects values other than MeshBridgeNetworkTCP
and MeshBridgeNetworkUDP. Keep the capability in memory and never
log, persist, or return it as module evidence.
Use a provider-native task or session instead for protocols that cannot preserve
TCP stream or UDP datagram semantics. The full decision flow starts at
Build a Module or Provider.
A survey module
package main
import (
"fmt"
"github.com/vibepwners/hovel/sdk/go/hovel"
)
type MockSurvey struct{}
func (MockSurvey) Info() hovel.Info {
return hovel.Info{
Name: "mock-survey-go",
Version: "v0.0.0-example",
Type: hovel.TypeSurvey,
Summary: "Collect example target facts.",
Tags: []string{"example", "survey", "go"},
}
}
func (MockSurvey) Schema() hovel.Schema {
return hovel.Schema{
TargetConfig: []hovel.Requirement{
hovel.Req("target.host", "host", "Target host name or IP address."),
hovel.Req("target.port", "port", "Target TCP port."),
},
}
}
func (MockSurvey) Run(ctx *hovel.Context) (hovel.Result, error) {
host := ctx.InputString("target.host", ctx.Target)
port := ctx.InputString("target.port", "unknown")
ctx.Log.Info("connecting to target", "host", host, "port", port)
return hovel.Ok(
map[string]any{"facts": map[string]any{"host": host, "port": port, "reachable": true}},
hovel.WithSummary(fmt.Sprintf("example survey reached %s:%s", host, port)),
), nil
}
func main() { hovel.Serve(MockSurvey{}) }
ctx.InputString(key, default) resolves a config value as a string.
ctx.Log emits structured records (key/value pairs, like
log/slog) that reach the daemon as module/log
notifications.
Findings and artifacts
Build the result with functional options.
func (MockExploit) Run(ctx *hovel.Context) (hovel.Result, error) {
if ctx.InputString("failure_mode", "") == "execution" {
return hovel.Failed("example exploit failed during execution"), nil
}
return hovel.Ok(
map[string]any{"target": ctx.Target},
hovel.WithSummary("example exploit completed without target interaction"),
hovel.WithFindings(hovel.Finding{
Title: "example exploit path verified",
Severity: "info",
Detail: "exercised orchestration without touching a real target",
}),
hovel.WithArtifacts(hovel.InlineArtifact(
"mock-exploit-go-transcript.txt", "text/plain",
"example module invoked through daemon RPC",
)),
), nil
}
Installed payload records
A Go exploit or provider that installs a durable target-side payload should
return an explicit descriptor with hovel.WithInstalledPayloads.
Hovel validates and persists it; the module never writes installed payload
inventory directly.
payload := hovel.InstalledPayloadDescriptor{
Provider: "squatter",
PayloadID: "squatter/windows/x86/xp-sp3/tcp-bind/pe-exe",
PayloadVersion: "0.1.0",
Target: ctx.Target,
State: "installed",
Transport: "tcp-bind",
Endpoint: "10.0.0.42:9101",
SupportsReconnect: true,
Cleanup: &hovel.PayloadProviderRecord{
Schema: "squatter.cleanup.tcp_bind",
Descriptor: map[string]any{"remotePath": `C:\Windows\Temp\n4x9q2.exe`},
},
}
return hovel.Ok(
map[string]any{"payload": payload.PayloadID},
hovel.WithSummary("payload installed and recorded"),
hovel.WithInstalledPayloads(payload),
), nil
Interactive sessions
LineShellSession needs no subclassing in Go — set its
Handle closure. ctx.OpenSession registers it and returns a
ref; the SDK attaches the ref to the result automatically.
func (MockExploitSession) Run(ctx *hovel.Context) (hovel.Result, error) {
shell := &hovel.LineShellSession{
Prompt: "mock$ ",
Echo: true,
Handle: func(command string) (string, error) {
switch command {
case "whoami":
return "mock-operator", nil
default:
return "mock-shell: command not found: " + command, nil
}
},
}
ref, err := ctx.OpenSession(shell,
hovel.WithName("mock shell on "+ctx.Target),
hovel.WithCapabilities("read", "write", "exec", "close"),
)
if err != nil {
return hovel.Result{}, err
}
return hovel.Ok(
map[string]any{"sessionId": ref.ID},
hovel.WithSummary("mock exploit opened an interactive shell session"),
), nil
}
For modules that already wrap a local interactive client, use
PTYSession on supported platforms. It exposes a local
pseudoterminal through the same Hovel session broker, so operator input and
output still travel through session/write and
session/read instead of raw stdout.
Step and payload providers
Go is the best-supported SDK for payload providers and capability-native
post-exploitation steps. Implement StepProvider when the module
should expose planner-visible steps; implement PayloadProvider
when it also owns payload discovery, generation, reconnect, cleanup, and
payload chunking.
type StepProvider interface {
hovel.Module
DescribeSteps() (hovel.StepContractSet, error)
PrepareStep(hovel.StepPrepareRequest) (hovel.StepPrepareResult, error)
ExecuteStep(hovel.StepExecuteRequest) (hovel.StepExecuteResult, error)
CleanupStep(hovel.StepCleanupRequest) (hovel.StepCleanupResult, error)
}
DescribeSteps is called during catalog inspection and planning, so
it must be pure. PrepareStep may materialize editable planned
values such as usernames, passwords, service names, pipe names, or paths, but
it must not touch the target. ExecuteStep consumes confirmed
prepared values exactly and returns capabilities, evidence, sessions, state
transitions, and installed payload descriptors. CleanupStep is
retriable and should report explicit state transitions rather than hiding
partial cleanup failures.
The sdk/go/hoveltest package has provider-oriented test helpers;
use it for contract tests that assert payload listing, resolution, session
connection, and cleanup behavior without starting a real daemon.
Test without the daemon
Start with ordinary Go unit tests against Info,
Schema, and Run. Build a hovel.Context
directly and assert result status, outputs, findings, artifacts, and
installed payload descriptors.
func TestMockExploitFailureMode(t *testing.T) {
ctx := &hovel.Context{
Target: "mock://target",
Inputs: map[string]any{"failure_mode": "execution"},
}
result, err := MockExploit{}.Run(ctx)
if err != nil {
t.Fatal(err)
}
if result.Status != "failed" {
t.Fatalf("status = %q, want failed", result.Status)
}
}
Use hoveltest.NewRPCConn when you need to test the actual framed
JSON-RPC surface, including handshake, schema, step methods, payload-provider
methods, and shutdown behavior.
The Go SDK and in-repo Go examples now live outside the core Bazel
workspace. The root Task dispatcher still exposes them through
task sdk:test, task sdk:ci,
task modules:examples:test, and
task modules:examples:ci.
Build, package, and run
Go modules build with Bazel (BUILD files are generated by Gazelle via
task fmt). The release package carries the compiled binary and a
hovel-module.yaml launch entry for each supported host platform.
go_binary(
name = "mock_survey",
embed = [":mock_survey_lib"],
)
apiVersion: hovel.dev/v1alpha1
kind: ModulePackage
metadata:
name: mock-survey-go
version: 0.1.0
moduleType: survey
runtime:
protocol: jsonrpc-stdio
launch:
- selector:
os: linux
arch: amd64
command: ["bin/linux-amd64/mock-survey-go"]
- selector:
os: darwin
arch: arm64
command: ["bin/darwin-arm64/mock-survey-go"]
hovel module install --link "$PWD/modules/examples/go/mock_survey"
task start
hovel> module installed # ... mock-survey-go, mock-exploit-go, mock-exploit-session-go
Run task sdk:ci and task modules:examples:ci before
submitting SDK or example changes. task modules:ci covers the
complete module slice, and task release:modules-package builds the
installable module archives and indexes. Add core test targets to
core/BUILD.bazel and module tests to their slice-level suites.