Shep Orchestration: How the Host Drives Execution
Not logged in

The Core Question

If "parse" is a capability but "Inventory" is a domain type, how do we organize this? What does the host actually do? What drives execution forward?

The Host Is the Policy Engine

The host owns policy — the decisions about what, when, and in what order. Plugins own mechanism — how to do specific things.

┌─────────────────────────────────────────────────────────────────┐
│                         SHEP HOST                               │
│                                                                 │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐              │
│  │ Playbook    │  │ Execution   │  │ State       │              │
│  │ Interpreter │  │ Loop        │  │ Manager     │              │
│  └─────────────┘  └─────────────┘  └─────────────┘              │
│         │                │                │                     │
│         ▼                ▼                ▼                     │
│  ┌─────────────────────────────────────────────────┐            │
│  │              Capability Router                  │            │
│  │   (routes to plugins by capability name)        │            │
│  └─────────────────────────────────────────────────┘            │
└─────────────────────────────────────────────────────────────────┘
          │              │              │              │
          ▼              ▼              ▼              ▼
    ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐
    │ parse    │  │ inventory│  │ execute  │  │ facts    │
    │ (plugin) │  │ (plugin) │  │ (plugin) │  │ (plugin) │
    └──────────┘  └──────────┘  └──────────┘  └──────────┘

What the Host Owns

1. The Execution Loop

The host runs the main loop. This is not delegated to plugins.

func (h *Host) RunPlaybook(playbookPath string) error {
    // 1. Parse the playbook (delegates to plugin, gets domain types back)
    playbook, err := h.parse(playbookPath)

    // 2. For each play in the playbook
    for _, play := range playbook.Plays {
        // 3. Resolve which hosts match this play
        hosts := h.resolveHosts(play.Hosts, h.inventory)

        // 4. For each task in the play
        for _, task := range play.Tasks {
            // 5. Evaluate conditions (host owns this logic)
            if !h.evaluateWhen(task.When, host) {
                continue
            }

            // 6. Execute on each host (delegates to plugin)
            for _, host := range hosts {
                result := h.execute(task, host)

                // 7. Handle result (host owns error policy)
                h.handleResult(result, task, host)
            }
        }

        // 8. Run handlers (host owns notification logic)
        h.runNotifiedHandlers(play)
    }

    return h.summarize()
}

2. Variable Resolution

The host owns variable precedence and scoping:

Command line vars (-e)        ← highest priority
Playbook vars
Play vars
Role vars
Host vars (inventory)
Group vars (inventory)
Role defaults                 ← lowest priority

This is policy. Plugins don't decide variable precedence.

3. Conditional Evaluation

The host evaluates when conditions:

- name: Install nginx
  apt: name=nginx
  when: ansible_os_family == "Debian"

The host:

4. Error Handling Policy

The host decides what happens on failure:

- name: Might fail
  command: /bin/false
  ignore_errors: yes # Host interprets this

- block:
    - name: Try this
      command: /bin/risky
  rescue: # Host implements rescue logic
    - name: Handle failure
      debug: msg="Failed, recovering"

5. State Management

The host tracks:

What Plugins Own

Parse Capability

Input: File path or raw bytes + format hint Output: Domain types (Playbook, Inventory, TaskList)

// Host calls
playbook, err := h.capabilities.Invoke("parse", &ParseRequest{
    FilePath: "site.yml",
    Format:   "yaml",
})

// Plugin returns strongly-typed domain objects
// The plugin knows HOW to parse YAML/JSON/INI
// The host knows WHAT to do with the result

The parser plugin doesn't know about execution order, variable precedence, or error handling. It just transforms bytes into domain objects.

Inventory Capability

Input: Inventory source (file, script, cloud API) Output: Inventory domain type

// Static file inventory
inv, _ := h.capabilities.Invoke("inventory.file", &InventoryRequest{
    Source: "hosts.yml",
})

// Dynamic cloud inventory
inv, _ := h.capabilities.Invoke("inventory.aws", &InventoryRequest{
    Region: "us-east-1",
    Tags:   map[string]string{"env": "prod"},
})

Different inventory plugins know HOW to discover hosts. The host knows WHAT to do with the discovered hosts.

Execute Capability

Input: Task + Host + resolved variables Output: Execution result (stdout, stderr, changed, failed)

result, err := h.capabilities.Invoke("execute.shell", &ExecuteRequest{
    Task: task,
    Host: host,
    Vars: resolvedVars,
})

The executor plugin knows HOW to run a shell command on a host. The host knows WHEN to run it and what to do with the result.

The Orchestration Flow

User runs: shep playbook site.yml -i inventory.yml

1. HOST: Load inventory
   └─► invoke("parse", inventory.yml) → Inventory

2. HOST: Load playbook
   └─► invoke("parse", site.yml) → Playbook

3. HOST: For each Play in Playbook
   │
   ├─► HOST: Resolve hosts matching play.hosts pattern
   │   (host owns pattern matching logic)
   │
   ├─► HOST: Gather facts (if not disabled)
   │   └─► invoke("facts", host) → Facts
   │
   └─► HOST: For each Task in Play
       │
       ├─► HOST: Evaluate when condition
       │   (host owns expression evaluation)
       │
       ├─► HOST: Resolve variables for this task+host
       │   (host owns variable precedence)
       │
       ├─► HOST: Determine executor
       │   └─► Based on task.module, find capability
       │       "ansible.builtin.shell" → "execute.shell"
       │       "ansible.builtin.apt"   → "execute.apt"
       │
       ├─► invoke(executor, task, host, vars) → Result
       │
       └─► HOST: Handle result
           ├─► Update facts if task gathered any
           ├─► Register result if task.register set
           ├─► Mark handler as notified if task.notify set
           ├─► Mark host as failed if result.failed
           └─► Decide: continue, rescue, or abort

4. HOST: Run notified handlers
   (same task execution loop)

5. HOST: Print summary

Expressing Desired Outcome

The user expresses desired outcome through a playbook:

# site.yml - This is the "what I want" specification
- name: Configure web servers
  hosts: webservers
  become: yes

  tasks:
    - name: Install nginx
      apt:
        name: nginx
        state: present

    - name: Start nginx
      service:
        name: nginx
        state: started
        enabled: yes

The playbook is declarative intent:

The host translates intent into execution:

  1. Find hosts matching "webservers"
  2. For each host, check if nginx is installed
  3. If not, install it
  4. Check if nginx is running
  5. If not, start it

The plugins do the actual work. The host decides the order and handles the results.

Why This Division?

Plugins are replaceable mechanisms

You can swap execute.shell for execute.shell-over-ssh or execute.shell-in-container without changing the host logic.

Host policy is consistent

Error handling, variable precedence, and conditional evaluation work the same regardless of which plugins are used.

Domain types are the contract

Plugins produce and consume well-typed domain objects. The host routes data between plugins using these types.

Capability vs Domain: The Parse Example

Capability: parse — the ability to transform bytes into structured data

Domain types: Inventory, Playbook, TaskList — the structures that represent the data

Request type: ParseRequest — what the capability needs as input

Response type: ParseResponse — what the capability produces

┌─────────────────────────────────────────────────────────────┐
│  Capability Layer                                           │
│  "parse" capability with ParseRequest/ParseResponse         │
├─────────────────────────────────────────────────────────────┤
│  Domain Layer                                               │
│  Inventory, Host, Group, Task, Playbook, Play               │
├─────────────────────────────────────────────────────────────┤
│  Representation Layer                                       │
│  YAML, JSON, INI, TOML (formats the parser understands)     │
└─────────────────────────────────────────────────────────────┘

The parse capability knows about the representation layer (YAML) and produces domain layer objects. It doesn't know about the capability layer's routing — that's the host's job.

Summary

Responsibility Owner Why
Execution loop Host Core orchestration policy
Variable precedence Host Consistency across all tasks
Conditional evaluation Host Policy decision
Error handling Host Policy decision
State tracking Host Cross-task coordination
Host pattern matching Host Inventory is host-managed
Parsing files Plugin Mechanism (how to read YAML)
Executing commands Plugin Mechanism (how to run on target)
Gathering facts Plugin Mechanism (how to query system)
Cloud inventory Plugin Mechanism (how to query AWS/GCP)

The host is the brain. Plugins are the hands. Domain types are the language they share.