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:
- Gathers facts (via a plugin)
- Stores them in state
- Evaluates the expression
- Decides whether to run the task
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:
- Which hosts have failed
- Which hosts are unreachable
- Facts gathered from each host
- Variables registered from task results
- Which handlers have been notified
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:
- "I want nginx installed"
- "I want nginx running and enabled"
The host translates intent into execution:
- Find hosts matching "webservers"
- For each host, check if nginx is installed
- If not, install it
- Check if nginx is running
- 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.