Skip to content

Hand catalog inputs to the team's playbook as data, never as templates (#181) - #189

Merged
shipmill-romamo[bot] merged 1 commit into
mainfrom
fix/181-catalog-inputs-as-data
Oct 31, 2026
Merged

shipmill-romamo[bot] merged 1 commit into
mainfrom
fix/181-catalog-inputs-as-data

Conversation

@shipmill-romamo

Copy link
Copy Markdown
Contributor

Summary

An approved catalog operation's inputs reached the team's own playbook as plain --extra-vars JSON (run_command in sdk/src/cloudfall/decision.py), and Ansible templates extra-var values: an input holding {{ lookup('pipe', 'touch /tmp/x') }} ran on the management host with the operator's rights when the playbook started, and an honest input holding {{ broke the run.

  • run_command marks every input string unsafe with cloudfall.ansible_data.as_ansible_data (the helper Hand declared config strings to Ansible as data, never as templates (#174) #179 added for the rendered inventory), so the playbook gets the text as written; integers and booleans keep their types. This is the one place that builds the inputs' --extra-vars, used by operations propose (check mode), decisions approve (run and verify) and agent investigate, which proposes through it
  • The replay (recording._invocation) reads the argv back with object_hook=unmarked, so its key matches the unmarked inputs a recording keeps. Recordings store the decision record's spec.inputs, which stay unmarked, so a recording made before this change and one made after match the same live invocation
  • The decision record's stored inputs, its schema, and the D-1/D-7 proposal matching (which compares the stored inputs) are unchanged; only the argv form changes
  • The SDK's own extra vars for the engine playbooks are out of scope, as Hand declared config strings to Ansible as data, never as templates (#174) #179 left them

Behaviour change: a team playbook that relied on an input being evaluated as Jinja now gets the literal text.

Plan

  • Serves: quality bar
  • Files: sdk/src/cloudfall/decision.py, sdk/src/cloudfall/recording.py, sdk/tests/test_decision.py, sdk/tests/test_host_stats.py, changelog.d/181-catalog-inputs-as-data.md
  • Order: a failing test first (a real ansible-playbook run of a proposal whose string input holds {{ lookup('pipe', 'touch <marker>') }}, plus integer and boolean inputs), then mark the inputs with as_ansible_data in run_command, then read them back with object_hook=unmarked in the replay's _invocation
  • Proof: the new test fails on main (the marker is created) and passes after; the existing replay test (test_a_replay_leaves_what_the_callback_would_have_written) keeps matching a recording; a new replay test matches a recording against a live invocation with a {{ input

Test plan

  • test_inputs_reach_the_playbook_as_data_never_as_templates (new, sdk/tests/test_decision.py): runs a real ansible-playbook through propose; fails on main (marker created), passes with the fix; the play sees the literal payload, 3 as an integer and true as a boolean
  • test_a_replay_matches_inputs_holding_jinja_as_recorded (new, sdk/tests/test_host_stats.py) and the existing replay test both fail when the object_hook=unmarked read is removed, and pass with it
  • uv run ruff check ., uv run mypy: clean. uv run ruff format --check . reports 72 files on main already (not a CI step); the lines this PR adds are formatted
  • uv run pytest sdk/tests/test_decision.py sdk/tests/test_host_stats.py sdk/tests/test_investigation.py sdk/tests/test_operator.py: 194 passed
  • uv run pytest engine/tests: 319 passed, 1 skipped
  • The full suite is left to CI

Fixes #181

🤖 Generated with Claude Code

#181)

An approved operation's inputs reached ansible-playbook as plain
--extra-vars JSON, and Ansible templates extra vars: an input holding
{{ lookup('pipe', ...) }} ran on the management host with the
operator's rights, and an honest input holding {{ broke the run.

run_command now marks every input string unsafe with as_ansible_data,
as #179 did for the rendered inventory; integers and booleans keep
their types. The replay reads the argv back with object_hook=unmarked,
so its key matches the unmarked inputs a recording keeps, from before
this change and after. The decision record's inputs are unchanged.

Fixes #181

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shipmill-romamo
shipmill-romamo Bot merged commit b55c438 into main Oct 31, 2026
1 check passed
@shipmill-romamo
shipmill-romamo Bot deleted the fix/181-catalog-inputs-as-data branch October 31, 2026 08:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

An approved catalog operation's inputs are Ansible-templated on the management host

0 participants