Skip to content

Extending Soul Stack

The base set of core modules covers the common operations — packages, files, services, users, cron, mounts, and so on. When the integration you need is not among them, Soul Stack extends through plugins — separate binaries that plug into the system over a single protocol, without modifying the core.

This is the same path to parity with the ecosystems of mature tools: the core set is native Go code, while niche and organization-specific integrations are written as plugins through the public Go SDK. The SDK and plugins are under Apache 2.0; you are free to publish your own module under any license, including a proprietary one. The core itself is under BSL 1.1 (fair-code), but that does not extend to plugins: they are separate processes over gRPC, not a derivative work of the core.

Core modulePlugin
Where it livesstatically built into the soul / keeper binarya separate executable
Installationnot needed, version = binary versionshipped as a standalone artifact
Who writes itships with Soul Stackyou, your organization, or the community
LanguageGo (in the core)any language with gRPC; the Go SDK is ready-made
Isolationin the host process’s address spacea separate process, gRPC over a socket

The catalog of built-in modules is in Modules. This section is about adding your own.

A plugin is a separate executable that the host process (soul or keeper) launches as a child process (subprocess) for the duration of an operation. Communication goes not through stdin/stdout parsing but over gRPC via a Unix domain socket:

  1. The host launches the plugin binary as a child process and passes it the socket path via an environment variable.
  2. On startup the plugin writes a single handshake line to stdout — JSON with a magic marker field, the protocol version, and the socket address. The host ignores every line before it (protection against stray output in init() or from libraries).
  3. From there the host and the plugin talk over gRPC on that socket.
  4. When the operation finishes, the host shuts the plugin down cleanly with a signal (graceful shutdown with a grace period).

The lifecycle is one-shot: the plugin comes up for an operation and exits after it. This is the same “one-line handshake → gRPC-over-socket” model used by Terraform providers and Vault plugins (the idea, not the code: the hashicorp/go-plugin library is not pulled in as a dependency, the format is our own).

The socket is a file with 0700 permissions, owned by the host process’s service user; it lives in a host-managed directory. The channel is protected at the filesystem-permission level: another process physically cannot open the socket. TLS is not used on the plugin socket — for a Unix socket with 0700 permissions it buys nothing.

The infrastructure is the same for all three types — the same handshake, launch mechanism, manifest format, and versioning. Only the gRPC contract (the set of methods) the plugin implements differs, and who launches it.

Type (kind)BinaryHostWhat it does
SoulModulesoul-mod-<name>the soul agent (on the host)Implements a Destiny step: brings a resource to a state (Validate / Apply, idempotently).
CloudDriversoul-cloud-<provider>keeper (on the server)Creates / deletes / queries VMs at a cloud provider.
SshProvidersoul-ssh-<provider>keeper (on the server)Supplies SSH credentials for push mode.

The binary name (soul-mod-* / soul-cloud-* / soul-ssh-*) is a convention; the plugin type is determined by the kind field in its manifest.

Plugins are written in Go through the public SDK. It handles all the protocol mechanics so the plugin author writes only business logic:

  • a handshake helper — reads the socket path, writes the handshake line, brings up the gRPC server, handles the shutdown signal;
  • typed interfaces for each of the three contracts (SoulModule / CloudDriver / SshProvider);
  • the plugin manifest types.

The dependency is minimal: the plugin author imports only the SDK and plugin-contract modules — no Keeper or Soul-agent code. More on building and the interface is in How to write a SoulModule.

The skeleton of a new SoulModule plugin is generated by soul-lint plugin-init — it gives you a ready project tree, and all that is left is to write Apply.

Next to the binary is a static manifest.yaml. It describes the plugin without running it: the type (kind), the protocol version, the name, the supported states and their parameters, the capabilities required from the host (required_capabilities), and the resources it touches (side_effects).

soul-lint reads the manifest during static Destiny checks — it catches an unknown module, an unknown state, wrong parameters, and missing permissions before anything is applied on hosts, without starting the plugin process. More on the manifest format is in How to write a SoulModule.