Skip to main content

Lift Ansible playbooks into Burr state machines with audit, replay, and structural introspection.

Project description

Philip

Lift operational artifacts into Burr state machines with audit, replay, and structural introspection. Drive them under FSM gating with an MCP-mounted agent via Theodosia, or run them locally as regular Python applications.

Supported sources today, into the Burr state-machine substrate:

  • Ansible playbooks (YAML) — deterministic lift through philip.from_playbook(path)
  • Mermaid stateDiagram-v2 (.mmd / .mermaid) — deterministic lift through philip.from_mermaid(path). Useful for converting a README's diagram into a runnable, agent-drivable FSM in one line.

Supported sources today, into the Hamilton dataflow-DAG substrate (install the hamilton extra):

  • SQL with CTEs — deterministic lift through philip.from_sql_cte(sql). Each CTE becomes a Hamilton function; external tables become Driver inputs. The result is a runnable, visualizable, type-checkable DAG for a query that was previously a single 500-line blob.

More sources land in subsequent minor releases: AWS Step Functions ASL, BPMN, dbt manifests, and additional DAG sources. Hamilton itself ships no from_X of its own; Burr ships no from_X of its own. Philip is the lift layer both substrates were missing.

import philip

# Ansible YAML -> Burr Application
app = philip.from_playbook("site.yml")

# Mermaid diagram -> Burr Application
app = philip.from_mermaid("docs/incident_response.mmd")

last, _, state = app.run(halt_after=["done", "escalate"])

report = philip.inspect("site.yml")
print(report.rendered_markdown())

# SQL with CTEs -> Hamilton module
module = philip.from_sql_cte(open("query.sql").read())
from hamilton.driver import Driver
Driver({}, module).visualize_execution(["query"], output_file_path="dag.png")

Why

Today there are two choices for AI-operated infrastructure. Run a playbook unattended (rigid, no judgment, can't adapt to context). Or let a free-form agent run shell commands (no constraint, no audit). Both extremes are wrong for most real ops work.

A third option exists. The playbook author writes the contract: which steps are reachable, in what order, where verification must happen, which failures route to recovery. The model (or human) operates within that contract. It picks, interprets logs, refuses when unsure, composes with other tools. It cannot invent steps, skip verification, or escape the procedure.

Three independent sources validate this architecture:

  • STRATUS (IBM Research, NeurIPS 2025) demonstrated FSM-organized SRE agents beat free-form ones by at least 1.5x on AIOpsLab and ITBench.
  • ITBench (IBM Research, Apache 2.0) is the open benchmark suite the STRATUS work was evaluated on.
  • Wikimedia's Spicerack is the operational existence proof. The most transparent SRE organization in the public world looked at Ansible in 2014, decided it was not controllable enough for production response, and built a Python framework with explicit phases and structural error handling from scratch.

Burr is the structured-Python substrate Spicerack would have used if it existed in 2014. Philip lifts your existing Ansible playbooks onto that substrate without rewriting anything.

Install

uv add philip-machine

Pulls ansible-core and ansible-runner transitively. Install Ansible collections via ansible-galaxy for your modules of choice:

ansible-galaxy collection install community.general community.docker ansible.posix

What you get over ansible-playbook

Situation ansible-playbook Philip + Burr
Mid-procedure decisions on runtime context when: over already-known vars Explicit transitions; an agent or human picks based on full state
Resume from arbitrary point --start-at-task hack; registers lost Burr persister rebuilds full state including registers
Approval gates pause: is stdin-bound; AWX bolt-on First-class states; approver acts through the same surface as any actor
Auditable rationale Task logs only Every transition logged with actor's choice and state snapshot
Counterfactual replay None fork_at(sequence_id) walks the alternate path
Composition with non-Ansible work Shell out and pray Coordinate Ansible steps alongside any Python in the same audit surface
Refusal as a structural action Run or fail First-class refusal transitions; "I don't know, escalating" routes structurally

ansible-playbook remains correct for deterministic, unattended, batch runs. Philip is the right tool when the playbook has decision points, needs verification gates, must be auditable for postmortem, or composes with non-Ansible work.

Supported subset

Philip lifts a defined subset of Ansible playbook syntax. Supported:

  • One play per file (multi-play raises UnsupportedPlaybookConstruct).
  • Tasks with name, exactly one module reference, and a module-args dict.
  • when: predicates (string expressions; lists are AND-joined).
  • register: capturing the full module result into a state key.
  • become: per-task or per-play.
  • failed_when: and ignore_errors: as guard transitions.
  • gather_facts: yes lowers to a leading ansible.builtin.setup action.
  • Play-level vars: populate with_state(...).
  • block: (group-only) inlines its tasks with the block's when: AND-propagated to each inner task.
  • include_tasks: and import_tasks: with literal filesystem paths.
  • notify: and handlers: with a deferred handler gated on _last_changed.
  • loop: and with_items: with literal list values.
  • set_fact: (bare and FQCN).

The following raise UnsupportedPlaybookConstruct:

  • rescue: and always: (deferred to a later release).
  • loop_control:, with_dict:, with_fileglob:, with_subelements:.
  • Jinja-templated loop: values.
  • Jinja-templated include_tasks: paths.
  • import_role:, include_role:, include:.
  • roles: blocks, pre_tasks:, post_tasks:.
  • serial:, strategy:, max_fail_percentage:, any_errors_fatal:.
  • Multi-play files.

The supported subset covers the common single-play remediation and day-2 procedures the FSM lift adds value to.

Mermaid stateDiagram-v2

import philip

app = philip.from_mermaid("incident_response.mmd")

Or directly from text:

text = """
stateDiagram-v2
    [*] --> Acknowledge
    Acknowledge --> Investigate : on_alert
    Investigate --> Mitigate
    Investigate --> Escalate : severity == "critical"
    Mitigate --> Verify
    Verify --> Done
    Escalate --> Done
    Done --> [*]
"""
app = philip.from_mermaid_text(text)

Lifting rules:

  • [*] --> X declares the entrypoint. Exactly one is required.
  • X --> [*] marks X as a terminal. Every terminal routes through a synthesized done action so the Burr graph is closed.
  • A --> B is an unconditional transition.
  • A --> B : label carries an edge label. Labels that look like Python predicates (contain comparison operators, and, or, not, in, is) lift directly to burr.core.expr guards. Labels that look like event names lift to _choice == "<label>" guards when the source has multiple outbound edges; otherwise they are documentation only.
  • Comments (%%), classDef, class, note, direction, and state declarations without a body are ignored.
  • Composite states (state X { ... }) raise MermaidLiftError. Inline them in your diagram before lifting.

Pair with Theodosia to mount a diagram as an MCP server:

import philip
import theodosia

theodosia.mount(
    philip.from_mermaid("incident_response.mmd"),
    name="incident",
).run()

Compose with Theodosia

Theodosia mounts a Burr Application as an MCP server. Philip lifts an Ansible playbook to a Burr Application. The composition is two lines:

import philip
import theodosia

theodosia.mount(philip.from_playbook("site.yml"), name="nginx-deploy").run()

Now any MCP client (Claude Code, Cursor, fast-agent, mcphost, a custom agent built on the Agent SDK) can drive the playbook step by step. The FSM enforces structural constraints. The model picks transitions at branch points. Every step is audit-trailed. Forking at any sequence_id gives you counterfactual replay for postmortem.

Theodosia and Philip are independent packages. Philip does not depend on Theodosia.

CLI

philip run     <playbook.yml>                 # execute end to end
philip inspect <playbook.yml>                 # static report: variables + failure topology
philip inspect <playbook.yml> --format json
philip graph   <playbook.yml> [--format mermaid|dot|text]
philip lint    <playbook.yml>                 # dry-convert with structural summary
philip emit    <playbook.yml>                 # round-trip lift -> emit canonical YAML

API

import philip

# Lift
app = philip.from_playbook("site.yml")
yaml_text = philip.to_playbook(app)

# Static introspection
report = philip.inspect("site.yml")
report.variables                 # variable provenance DAG
report.undefined_variables       # references with no defining site
report.unused_definitions        # bound but never referenced
report.failure_topology          # per-action FAILURE_KIND routing
report.actions_with_recovery     # actions with a true recovery branch
report.unhandled_failures        # actions where a failure has no transition
report.rendered_markdown()       # human-readable report

# Hand-write actions backed by Ansible modules
@philip.module_action("ansible.builtin.command", reads=["target"], writes=["uptime"])
def get_uptime(state):
    return {"cmd": "uptime"}

# Or call modules directly
result = philip.run_module("ansible.builtin.ping", {}, host="myhost")

# Connection management
host = philip.host(ansible_host="example.com", ansible_user="deploy")

# Polling sub-graphs
wait = philip.wait_until(
    "ansible.builtin.wait_for",
    args={"port": 8080, "timeout": 1},
    max_attempts=30,
)

Failure classification

Every module-backed action writes structural failure sentinels into state on each step:

  • _last_action
  • _last_failed
  • _last_changed
  • _last_unreachable
  • _last_msg
  • _last_failure_kind (one of ok, unreachable, auth_failed, timeout, module_error)

Transitions branch on these without each action having to opt in via writes=. The classification is conservative and pattern-based: the unreachable and failed result flags are trusted; the diagnostic msg is scanned for known phrases that indicate auth and timeout. The labels are loosely aligned with the MAST taxonomy.

License

Apache 2.0.

Development

LLM-assisted development was used during construction.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

philip_machine-1.2.1.tar.gz (522.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

philip_machine-1.2.1-py3-none-any.whl (72.9 kB view details)

Uploaded Python 3

File details

Details for the file philip_machine-1.2.1.tar.gz.

File metadata

  • Download URL: philip_machine-1.2.1.tar.gz
  • Upload date:
  • Size: 522.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for philip_machine-1.2.1.tar.gz
Algorithm Hash digest
SHA256 d925d24b35e7bf95fa7ff1bc07e8b3dc8167718b7702659d05be0ed2f0b0815e
MD5 3298f75cca4c09bf35388b1142437104
BLAKE2b-256 7cd756505b9edae4854dbe945b9c3e0c6b27c51e02584e2e1c780b67e66855cd

See more details on using hashes here.

File details

Details for the file philip_machine-1.2.1-py3-none-any.whl.

File metadata

  • Download URL: philip_machine-1.2.1-py3-none-any.whl
  • Upload date:
  • Size: 72.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for philip_machine-1.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 605918b6a3814c8a2a0d950865a553f3568fcf958102458a517d2e5fa0d91cab
MD5 8a52c2be08d2c9ae476baa803cafe3cc
BLAKE2b-256 4263c39d4163b8d3923b5abc58a5e7d55704b397fcb266eeaa3c471cf54d6507

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page