Skip to content

fix(pickled-iac): use real 'tofu' binary name; opentofu was never an executable - #31

Merged
bartrosa merged 1 commit into
mainfrom
cursor/critical-correctness-bugs-cf01
Jun 9, 2026
Merged

bartrosa merged 1 commit into
mainfrom
cursor/critical-correctness-bugs-cf01

Conversation

@cursor

@cursor cursor Bot commented Jun 7, 2026

Copy link
Copy Markdown

Bug & impact

pickled-iac draft <story>, the validate_terraform_dir MCP tool, and plan() were completely broken for every user who had OpenTofu installed but not Terraform — i.e. anyone who installed brew install opentofu, apt install tofu, winget install OpenTofu.tofu, or the upstream install-opentofu.sh script. The pickled-iac README explicitly advertises both: "requires terraform or tofu on PATH for validate/plan/draft."

Reproduction (before the fix), with only tofu on PATH:

>>> import pickled_iac.oracle as o
>>> o.iac_binary()
'opentofu'
>>> o.validate(Path("./module"))
Traceback (most recent call last):
  ...
FileNotFoundError: [Errno 2] No such file or directory: 'opentofu'

For drafting that means the user pays for three LLM completions and then sees a hard crash on the very first attempt — every validate retry fails the same way. For the MCP tool the failure is silent from the LLM-host's perspective (subprocess error surfaces as a tool error).

Root cause

oracle.py conflated two distinct strings:

_IAC_BIN: Literal["terraform", "opentofu"] | None
if shutil.which("terraform"):
    _IAC_BIN = "terraform"
elif shutil.which("tofu"):
    _IAC_BIN = "opentofu"   # ← format label, NOT a real binary name
else:
    _IAC_BIN = None

def iac_binary() -> Literal["terraform", "opentofu"]:
    ...
    return _IAC_BIN

_IAC_BIN was then handed straight to subprocess.run([_IAC_BIN, "init", ...]). OpenTofu installs as the tofu executable — opentofu does not exist on any supported install path (verified via OpenTofu docs: https://opentofu.org/docs/intro/install/standalone/). The miss-detection slipped past CI because test_oracle.py only runs when terraform or tofu is on PATH, and the Terraform-only path was never broken.

IaCDrafter.draft_module() mirrored the same mistake (fmt = "opentofu" if binary == "opentofu" else "terraform").

Fix

Split the executable-name concern from the format-label concern:

  • iac_binary() -> Literal["terraform", "tofu"] — actual executable name; what subprocess.run needs.
  • iac_format() -> Literal["terraform", "opentofu"] — human-facing label used in ValidateResult.format, PlanResult.format, IaCArtifact.format.

validate(), plan(), and IaCDrafter.draft_module() all now call iac_format() for the label and iac_binary() for the subprocess; the brittle "opentofu" if binary == "opentofu" else "terraform" branches that silently encoded the bug are gone.

Validation

  • Manual end-to-end repro before the fix (fake tofu on PATH, no terraform): validate() raises FileNotFoundError: 'opentofu'. After: validate() returns ValidateResult(valid=True, format='opentofu') and spawns the real tofu executable.
  • New packages/pickled-iac/tests/test_oracle_binary.py (5 tests, run on every CI host because they don't require terraform/tofu installed):
    • iac_binary() resolves to "tofu" and iac_format() to "opentofu" when only OpenTofu is present.
    • iac_binary() / iac_format() both return "terraform" when Terraform is present.
    • iac_binary() raises IaCToolMissingError when neither is installed.
    • validate() end-to-end against a Python-shebang fake tofu binary — pre-flighted with a pytest.raises(FileNotFoundError) on the bogus opentofu name to keep the regression honest.
    • Source-level guard that asserts _IAC_BIN = "opentofu" is never re-introduced, so a future refactor cannot silently bring the bug back on terraform-only CI hosts.
  • Existing tests updated: test_drafter.py and test_llm_sanitize.py now patch iac_format alongside iac_binary and gain a coverage case for the "opentofu" label.
  • Full workspace suite: uv run pytest → 459 passed, 4 skipped.
  • mypy packages/pickled-iac/src/: clean (10 source files).
  • ruff check: clean for all touched files.
Open in Web View Automation 

…executable

The IaC oracle stored '"opentofu"' in _IAC_BIN when only OpenTofu was on
PATH and handed that string straight to subprocess.run. OpenTofu installs
as the 'tofu' binary, never 'opentofu' (per opentofu.org/docs/intro/install),
so every pickled-iac draft / validate / plan / validate_terraform_dir MCP
call raised FileNotFoundError: [Errno 2] No such file or directory:
'opentofu' on hosts that had only OpenTofu installed.

The miss-detection slipped past CI because test_oracle.py only runs when
'terraform' or 'tofu' is on PATH, and the Terraform-only path was never
broken.

Split the two concerns:

* iac_binary() now returns the actual executable name ('terraform' or
  'tofu') and feeds subprocess.run.
* iac_format() returns the human-facing format label ('terraform' or
  'opentofu') used in ValidateResult / PlanResult / IaCArtifact.

drafter.py + oracle.py now call iac_format() for the label; the
'"opentofu" if binary == "opentofu" else "terraform"' branches that
silently encoded the bug are gone.

Regression coverage:

* test_oracle_binary.py — five tests that exercise the resolution logic
  without requiring terraform/tofu on PATH, including an end-to-end
  validate() against a Python-shebang fake 'tofu' binary plus a
  source-level guard against re-introducing '_IAC_BIN = "opentofu"'.
* test_drafter.py / test_llm_sanitize.py — extended to patch iac_format
  alongside iac_binary so both the binary and label paths are pinned.

Full workspace suite: 459 passed, 4 skipped.
mypy + ruff clean for the touched files.

Co-authored-by: Bartłomiej Rosa <bartrosa@users.noreply.github.com>
@bartrosa
bartrosa marked this pull request as ready for review June 9, 2026 15:16
@bartrosa
bartrosa merged commit 11a7583 into main Jun 9, 2026
0 of 4 checks passed
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.

2 participants