Status: Implementing
1. Overview
Shep is an Ansible-like orchestration engine implemented in Go. It focuses on a plugin-forward architecture using the HashiCorp plugin model for process-isolated extensions and FlatBuffers for efficient, cross-language payloads over stdin/stdout.
Primary goals:
- Provide a lightweight, extensible runtime for running tasks and playbooks across hosts.
- Use HashiCorp's go-plugin architecture to allow plugins in multiple languages while keeping the core in Go.
- Use FlatBuffers for payload schemas to ensure compact, forward/backward-compatible messages and fast (de)serialization.
Non-goals:
- Reimplement configuration management UI; initial scope is CLI and library.
2. Constraints & Assumptions
- The core controller runs in Go 1.25+ (no Python runtime dependency).
- Plugins run as separate processes and communicate via stdin/stdout using FlatBuffers. There is no network transport; all communication is local IPC.
- FlatBuffers is the canonical schema language and wire format for plugin communication.
- Third-party plugins may be written in any language that can read/write FlatBuffers over stdin/stdout (Go, Rust, Python, etc.). This project provides Go tooling and SDKs only; other languages are community-supported.
3. High-level Components
- Controller (core): CLI, orchestration engine, scheduler, connection manager, inventory manager, logging.
- Plugin Host / Manager: starts and manages plugin subprocesses, performs handshake and protocol negotiation via stdin/stdout.
- Plugin Types:
- Action Modules: perform tasks on target hosts (idempotent where applicable).
- Inventory Plugins: provide host lists, host variables, groups.
- Connection Plugins: handle transport to remote targets (local for v1, SSH deferred).
- Filter/Lookup Plugins: transformation and data lookup utilities.
- Transport & Execution: a worker subsystem that coordinates running tasks against hosts, handling parallelism and retry policies.
Logging:
- Logging is a responsibility of the host controller.
- Plugins send log messages to the host via a dedicated FlatBuffer message type.
- The host aggregates and formats all log output.
4. Plugin Model (HashiCorp go-plugin)
Design decisions:
- Use HashiCorp's plugin framework (go-plugin) to achieve language-agnostic, process-isolated plugins.
- Communication uses stdin/stdout exclusively. There is no gRPC or network transport.
- FlatBuffers-serialized messages are exchanged bidirectionally over stdio.
Plugin discovery:
- The host process is named
shep. - Plugin binaries are named
shep-plugin-*(e.g.,shep-plugin-parser,shep-plugin-file). - Plugins are discovered in the same directory as the
shepbinary.
Handshake and lifecycle:
- The controller launches a plugin subprocess and performs the plugin handshake via stdin/stdout.
- After handshake, the controller exchanges typed FlatBuffer messages with the plugin.
- Each plugin implements the agreed interfaces (Action, Inventory, Connection, etc.).
Handshake schema:
table Handshake {
protocol_version:int;
plugin_name:string;
capabilities:[string];
}
Isolation & security:
- Run plugins as unprivileged processes where possible; provide sandboxing guidance (e.g., containers) for untrusted plugins.
Versioning & compatibility:
- Use a monotonic integer for FlatBuffers protocol version (1, 2, 3...).
- Increment only for breaking changes to handshake or message schema.
- Keep FlatBuffers schemas backward-compatible using optional fields and unions.
5. FlatBuffers Schema Strategy
Why FlatBuffers:
- Zero-copy access pattern for performance (where idiomatic in Go).
- Compact binary format for fast transmission.
- Official support for multiple languages via codegen.
Schema management:
- Store canonical
.fbsfiles inflatbuffers/. - Use codegen in CI to generate language bindings for Go and place generated
code under
internal/fbs/. - Each schema file includes a protocol version identifier.
Style guide (per https://flatbuffers.dev/schema/):
- Field names:
snake_case(lowercase with underscores) - Table/struct/enum names:
UpperCamelCase - Enum values:
UpperCamelCase - Namespaces:
lowercase(single word preferred)
Message design principles:
- Keep messages small and explicit (e.g., ExecuteTaskRequest, ExecuteTaskReply).
- Use unions for variant payloads (e.g., Result.Success | Result.Failure).
- Use typed fields instead of JSON strings where possible.
- Use enums instead of magic numbers.
- Provide a light metadata envelope around payloads with fields: protocol_version, message_type, correlation_id.
Example message flows (high-level):
- Controller -> Plugin: ExecuteTaskRequest{task, args, host, connection_info}
- Plugin -> Controller: ExecuteTaskReply{status, exit_code, stdout, stderr, changed, facts}
- Plugin -> Controller: LogMessage{level, message, timestamp}
6. Communication Flow and Data Exchange
- All communication between host and plugins uses FlatBuffer binary messages over stdin/stdout.
- There is no plugin-to-plugin communication. All plugin interactions are mediated by the host controller.
- The controller maps playbook constructs to messages sent to action plugins and connection plugins as needed.
Sequence (simple task run):
- Controller resolves inventory and selects target hosts.
- Controller ensures a Connection plugin is available for transport to the host.
- Controller prepares an ExecuteTaskRequest FlatBuffer and writes it to the Action plugin's stdin.
- Plugin runs task and writes ExecuteTaskReply to stdout.
- Controller reads the reply, records status, and handles logging.
7. Data Model
- Playbook: Ansible-compatible YAML parsed by the controller (via parser plugin).
- Tasks: structured objects (name, module, args, when, register, become, delegate_to).
- Inventory: canonical host objects (hostname, address, vars, groups, connection hints).
Ansible compatibility focus:
- https://json.schemastore.org/ansible-playbook
- https://json.schemastore.org/ansible-tasks-file
- https://json.schemastore.org/ansible-inventory
Deferred for later:
- https://json.schemastore.org/ansible-role-2.9
- https://json.schemastore.org/ansible-collection-galaxy
Controller stores runtime state in-memory; optional persistence can be added later (e.g., local SQLite for job history).
8. Security Considerations
- Plugin execution: run with least privilege; document expectations for trusted/untrusted plugins.
- Transport credentials: to be defined in later versions.
- Authentication: plugin handshake validates protocol versions.
Note: Detailed security mechanisms (secret handling, plugin signing, network isolation) are deferred beyond v1.
9. Developer Experience & Tooling
- Code generation:
- Add
make gen-schemasto generate Go bindings from.fbsfiles. - Publish plugin-helper SDKs for Go that wrap the raw FlatBuffers payloads and provide typed APIs.
- Add
- Local development:
- Provide
shep plugin initto scaffold a plugin in Go with example schemas and build/test scripts.
- Provide
- Tests:
- Unit tests for controller logic.
- Contract tests for plugins using a lightweight plugin harness that asserts schema conformance.
Note: Advanced developer tooling is deferred beyond v1.
10. Project Layout (recommended)
Recommended repo layout (root: shep/ is this repo):
shep/
├── cmd/
│ └── shep/ # main CLI entrypoint
│ └── main.go
├── internal/ # shared internal packages
│ ├── host/ # plugin host logic (orchestration, handshake)
│ ├── fbs/ # FlatBuffers-generated code (shared schemas)
│ ├── util/ # misc helpers
│ └── test/ # shared test helpers
├── plugins/ # core plugins (all Go binaries)
│ ├── parser/ # parses Ansible-compatible YAML/JSON
│ │ ├── main.go
│ │ ├── plugin.go # plugin-specific logic
│ │ ├── testdata/ # test inputs/outputs for parser plugin
│ │ └── README.md
│ └── ... # other plugins (file, command, etc.)
├── flatbuffers/ # FlatBuffers schema files
│ ├── shep.fbs
│ └── generate.go # optional script to generate Go code
├── docs/
│ ├── cli.md # shep CLI usage
│ └── plugins.md # overview of all plugins
├── testdata/ # global testdata (playbooks, example inputs)
├── go.mod
├── go.sum
└── Makefile # build/test/documentation targets
11. CI/CD and Testing
- CI steps:
- Validate
.fbscompile. - Run
make gen-schemasand verify generated code builds. - Run unit tests across core and plugin harness.
- Validate
- Contract testing:
- Provide a contract test that any plugin must pass: given sample ExecuteTaskRequest, return a legal ExecuteTaskReply.
12. Roadmap / Milestones
Phase 0 — Spec & PoC
- Define canonical FlatBuffers schemas (inventory, task, result, log).
- Implement controller skeleton and plugin manager.
- Create a Go action plugin PoC and demonstrate ExecuteTask roundtrip over stdin/stdout.
Phase 1 — MVP (Local Execution)
- Playbook YAML parser with a small subset of features (task, args, hosts, delegate_to).
- Inventory plugin example.
- Local connection plugin only (no SSH).
- Simple local task execution.
Phase 2 — Remote Execution & Ecosystem
- Connection plugin (SSH) and remote execution.
- Publish plugin SDKs for Go and Rust.
- Add plugin discovery/registry and signing guidance.
- Add advanced features (become, facts, handlers, rolling updates).
13. Migration and Compatibility
- Use protocol version fields in FlatBuffers to enable graceful upgrades.
- Keep plugin interfaces stable; provide an adapter layer in the controller for newer/older protocols.
Note: Deprecation policy deferred beyond v1.
14. Appendix: Example FlatBuffers Schema
The following is a compact example of the kinds of messages used; expand and
normalize in flatbuffers/.
namespace shep;
enum Status : byte { Ok = 0, Failure = 1 }
enum LogLevel : byte { Debug = 0, Info = 1, Warn = 2, Error = 3 }
table Metadata {
protocol_version:int = 1;
message_type:string;
correlation_id:string;
}
table KeyValue {
key:string;
value:string;
}
table ExecuteTaskRequest {
meta:Metadata;
task_name:string;
args:[KeyValue];
host:string;
connection_info:[KeyValue];
}
table ExecuteTaskReply {
meta:Metadata;
status:Status;
exit_code:int;
stdout:[ubyte]; // UTF-8 encoded
stderr:[ubyte]; // UTF-8 encoded
changed:bool;
error_message:string;
}
table LogMessage {
meta:Metadata;
level:LogLevel;
message:string;
timestamp:long; // Unix timestamp in milliseconds
}
root_type ExecuteTaskRequest;
Notes:
- Use typed container fields (KeyValue tables) instead of freeform JSON strings.
- stdout/stderr are UTF-8 encoded byte vectors.
- Use streaming for very large outputs.
15. Next Steps
- Finalize canonical schemas and commit to
flatbuffers/. - Scaffold controller core (CLI + plugin manager) and a simple Go action plugin PoC.
- Add CI rule to generate and test schema bindings.
File: shep-arch.md