Clarifies the rewrite decision to what it actually meant: rewriting the bash in
Python does not mean reimplementing the operating system. A guarded exec is the
right answer for rustup, curl, unzip, git, godot, blender. What must become
Python is the LOGIC — which version is wanted, whether it is already present,
what the output means, what to do when it fails. The test of a correct port is
not whether it calls anything external, but whether the decisions can be
exercised without performing them.
Delivered ahead of the remaining ports because every one of them needs it.
core/process.run is the single sanctioned exec, and each of its guards exists
because a per-domain subprocess call is precisely where that guard goes
missing:
- An argv list, never a shell string. A string is rejected outright rather than
helpfully split, since the helpful split is the vulnerability.
- shell=False always.
- A non-zero exit becomes a ReachError naming the command, carrying its output,
and preserving its exit code — not a CalledProcessError traceback at someone
who wanted to know the next step.
- A missing binary reports what to install. FileNotFoundError names the path
that was not found, which is the less useful half of the answer.
All four verified against real commands, including a genuine git failure
relaying exit 128.
A conformance invariant keeps the door single: nothing outside core/process.py
may import subprocess or call os.system/popen/exec*. Proven to fail by
importing subprocess into a domain service.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>